| // 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. |
| |
| package firestore |
| |
| import ( |
| "errors" |
| "flag" |
| "fmt" |
| "log" |
| "math" |
| "os" |
| "path/filepath" |
| "runtime" |
| "sort" |
| "testing" |
| "time" |
| |
| "cloud.google.com/go/internal/pretty" |
| "cloud.google.com/go/internal/testutil" |
| "cloud.google.com/go/internal/uid" |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| |
| "golang.org/x/net/context" |
| "google.golang.org/api/option" |
| "google.golang.org/genproto/googleapis/type/latlng" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/metadata" |
| ) |
| |
| func TestMain(m *testing.M) { |
| initIntegrationTest() |
| status := m.Run() |
| cleanupIntegrationTest() |
| os.Exit(status) |
| } |
| |
| const ( |
| envProjID = "GCLOUD_TESTS_GOLANG_FIRESTORE_PROJECT_ID" |
| envPrivateKey = "GCLOUD_TESTS_GOLANG_FIRESTORE_KEY" |
| ) |
| |
| var ( |
| iClient *Client |
| iColl *CollectionRef |
| collectionIDs = uid.NewSpace("go-integration-test", nil) |
| ) |
| |
| func initIntegrationTest() { |
| flag.Parse() // needed for testing.Short() |
| if testing.Short() { |
| return |
| } |
| ctx := context.Background() |
| testProjectID := os.Getenv(envProjID) |
| if testProjectID == "" { |
| log.Println("Integration tests skipped. See CONTRIBUTING.md for details") |
| return |
| } |
| ts := testutil.TokenSourceEnv(ctx, envPrivateKey, |
| "https://www.googleapis.com/auth/cloud-platform", |
| "https://www.googleapis.com/auth/datastore") |
| if ts == nil { |
| log.Fatal("The project key must be set. See CONTRIBUTING.md for details") |
| } |
| ti := &testInterceptor{dbPath: "projects/" + testProjectID + "/databases/(default)"} |
| c, err := NewClient(ctx, testProjectID, |
| option.WithTokenSource(ts), |
| option.WithGRPCDialOption(grpc.WithUnaryInterceptor(ti.interceptUnary)), |
| option.WithGRPCDialOption(grpc.WithStreamInterceptor(ti.interceptStream)), |
| ) |
| if err != nil { |
| log.Fatalf("NewClient: %v", err) |
| } |
| iClient = c |
| iColl = c.Collection(collectionIDs.New()) |
| refDoc := iColl.NewDoc() |
| integrationTestMap["ref"] = refDoc |
| wantIntegrationTestMap["ref"] = refDoc |
| integrationTestStruct.Ref = refDoc |
| } |
| |
| type testInterceptor struct { |
| dbPath string |
| } |
| |
| func (ti *testInterceptor) interceptUnary(ctx context.Context, method string, req, res interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { |
| ti.checkMetadata(ctx, method) |
| return invoker(ctx, method, req, res, cc, opts...) |
| } |
| |
| func (ti *testInterceptor) interceptStream(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { |
| ti.checkMetadata(ctx, method) |
| return streamer(ctx, desc, cc, method, opts...) |
| } |
| |
| func (ti *testInterceptor) checkMetadata(ctx context.Context, method string) { |
| md, ok := metadata.FromOutgoingContext(ctx) |
| if !ok { |
| log.Fatalf("method %s: bad metadata", method) |
| } |
| for _, h := range []string{"google-cloud-resource-prefix", "x-goog-api-client"} { |
| v, ok := md[h] |
| if !ok { |
| log.Fatalf("method %s, header %s missing", method, h) |
| } |
| if len(v) != 1 { |
| log.Fatalf("method %s, header %s: bad value %v", method, h, v) |
| } |
| } |
| v := md["google-cloud-resource-prefix"][0] |
| if v != ti.dbPath { |
| log.Fatalf("method %s: bad resource prefix header: %q", method, v) |
| } |
| } |
| |
| func cleanupIntegrationTest() { |
| if iClient == nil { |
| return |
| } |
| // TODO(jba): delete everything in integrationColl. |
| iClient.Close() |
| } |
| |
| // integrationClient should be called by integration tests to get a valid client. It will never |
| // return nil. If integrationClient returns, an integration test can proceed without |
| // further checks. |
| func integrationClient(t *testing.T) *Client { |
| if testing.Short() { |
| t.Skip("Integration tests skipped in short mode") |
| } |
| if iClient == nil { |
| t.SkipNow() // log message printed in initIntegrationTest |
| } |
| return iClient |
| } |
| |
| func integrationColl(t *testing.T) *CollectionRef { |
| _ = integrationClient(t) |
| return iColl |
| } |
| |
| type integrationTestStructType struct { |
| Int int |
| Str string |
| Bool bool |
| Float float32 |
| Null interface{} |
| Bytes []byte |
| Time time.Time |
| Geo, NilGeo *latlng.LatLng |
| Ref *DocumentRef |
| } |
| |
| var ( |
| integrationTime = time.Date(2017, 3, 20, 1, 2, 3, 456789, time.UTC) |
| // Firestore times are accurate only to microseconds. |
| wantIntegrationTime = time.Date(2017, 3, 20, 1, 2, 3, 456000, time.UTC) |
| |
| integrationGeo = &latlng.LatLng{Latitude: 30, Longitude: 70} |
| |
| // Use this when writing a doc. |
| integrationTestMap = map[string]interface{}{ |
| "int": 1, |
| "str": "two", |
| "bool": true, |
| "float": 3.14, |
| "null": nil, |
| "bytes": []byte("bytes"), |
| "*": map[string]interface{}{"`": 4}, |
| "time": integrationTime, |
| "geo": integrationGeo, |
| "ref": nil, // populated by initIntegrationTest |
| } |
| |
| // The returned data is slightly different. |
| wantIntegrationTestMap = map[string]interface{}{ |
| "int": int64(1), |
| "str": "two", |
| "bool": true, |
| "float": 3.14, |
| "null": nil, |
| "bytes": []byte("bytes"), |
| "*": map[string]interface{}{"`": int64(4)}, |
| "time": wantIntegrationTime, |
| "geo": integrationGeo, |
| "ref": nil, // populated by initIntegrationTest |
| } |
| |
| integrationTestStruct = integrationTestStructType{ |
| Int: 1, |
| Str: "two", |
| Bool: true, |
| Float: 3.14, |
| Null: nil, |
| Bytes: []byte("bytes"), |
| Time: integrationTime, |
| Geo: integrationGeo, |
| NilGeo: nil, |
| Ref: nil, // populated by initIntegrationTest |
| } |
| ) |
| |
| func TestIntegration_Create(t *testing.T) { |
| ctx := context.Background() |
| doc := integrationColl(t).NewDoc() |
| start := time.Now() |
| h := testHelper{t} |
| wr := h.mustCreate(doc, integrationTestMap) |
| end := time.Now() |
| checkTimeBetween(t, wr.UpdateTime, start, end) |
| _, err := doc.Create(ctx, integrationTestMap) |
| codeEq(t, "Create on a present doc", codes.AlreadyExists, err) |
| // OK to create an empty document. |
| _, err = integrationColl(t).NewDoc().Create(ctx, map[string]interface{}{}) |
| codeEq(t, "Create empty doc", codes.OK, err) |
| } |
| |
| func TestIntegration_Get(t *testing.T) { |
| ctx := context.Background() |
| doc := integrationColl(t).NewDoc() |
| h := testHelper{t} |
| h.mustCreate(doc, integrationTestMap) |
| ds := h.mustGet(doc) |
| if ds.CreateTime != ds.UpdateTime { |
| t.Errorf("create time %s != update time %s", ds.CreateTime, ds.UpdateTime) |
| } |
| got := ds.Data() |
| if want := wantIntegrationTestMap; !testEqual(got, want) { |
| t.Errorf("got\n%v\nwant\n%v", pretty.Value(got), pretty.Value(want)) |
| } |
| |
| doc = integrationColl(t).NewDoc() |
| empty := map[string]interface{}{} |
| h.mustCreate(doc, empty) |
| ds = h.mustGet(doc) |
| if ds.CreateTime != ds.UpdateTime { |
| t.Errorf("create time %s != update time %s", ds.CreateTime, ds.UpdateTime) |
| } |
| if got, want := ds.Data(), empty; !testEqual(got, want) { |
| t.Errorf("got\n%v\nwant\n%v", pretty.Value(got), pretty.Value(want)) |
| } |
| |
| ds, err := integrationColl(t).NewDoc().Get(ctx) |
| codeEq(t, "Get on a missing doc", codes.NotFound, err) |
| if ds == nil || ds.Exists() { |
| t.Fatal("got nil or existing doc snapshot, want !ds.Exists") |
| } |
| if ds.ReadTime.IsZero() { |
| t.Error("got zero read time") |
| } |
| } |
| |
| func TestIntegration_GetAll(t *testing.T) { |
| type getAll struct{ N int } |
| |
| h := testHelper{t} |
| coll := integrationColl(t) |
| ctx := context.Background() |
| var docRefs []*DocumentRef |
| for i := 0; i < 5; i++ { |
| doc := coll.NewDoc() |
| docRefs = append(docRefs, doc) |
| if i != 3 { |
| h.mustCreate(doc, getAll{N: i}) |
| } |
| } |
| docSnapshots, err := iClient.GetAll(ctx, docRefs) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := len(docSnapshots), len(docRefs); got != want { |
| t.Fatalf("got %d snapshots, want %d", got, want) |
| } |
| for i, ds := range docSnapshots { |
| if i == 3 { |
| if ds == nil || ds.Exists() { |
| t.Fatal("got nil or existing doc snapshot, want !ds.Exists") |
| } |
| err := ds.DataTo(nil) |
| codeEq(t, "DataTo on a missing doc", codes.NotFound, err) |
| } else { |
| var got getAll |
| if err := ds.DataTo(&got); err != nil { |
| t.Fatal(err) |
| } |
| want := getAll{N: i} |
| if got != want { |
| t.Errorf("%d: got %+v, want %+v", i, got, want) |
| } |
| } |
| if ds.ReadTime.IsZero() { |
| t.Errorf("%d: got zero read time", i) |
| } |
| } |
| } |
| |
| func TestIntegration_Add(t *testing.T) { |
| start := time.Now() |
| _, wr, err := integrationColl(t).Add(context.Background(), integrationTestMap) |
| if err != nil { |
| t.Fatal(err) |
| } |
| end := time.Now() |
| checkTimeBetween(t, wr.UpdateTime, start, end) |
| } |
| |
| func TestIntegration_Set(t *testing.T) { |
| coll := integrationColl(t) |
| ctx := context.Background() |
| h := testHelper{t} |
| |
| // Set Should be able to create a new doc. |
| doc := coll.NewDoc() |
| wr1, err := doc.Set(ctx, integrationTestMap) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // Calling Set on the doc completely replaces the contents. |
| // The update time should increase. |
| newData := map[string]interface{}{ |
| "str": "change", |
| "x": "1", |
| } |
| wr2, err := doc.Set(ctx, newData) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !wr1.UpdateTime.Before(wr2.UpdateTime) { |
| t.Errorf("update time did not increase: old=%s, new=%s", wr1.UpdateTime, wr2.UpdateTime) |
| } |
| ds := h.mustGet(doc) |
| if got := ds.Data(); !testEqual(got, newData) { |
| t.Errorf("got %v, want %v", got, newData) |
| } |
| |
| newData = map[string]interface{}{ |
| "str": "1", |
| "x": "2", |
| "y": "3", |
| } |
| // SetOptions: |
| // Only fields mentioned in the Merge option will be changed. |
| // In this case, "str" will not be changed to "1". |
| wr3, err := doc.Set(ctx, newData, Merge([]string{"x"}, []string{"y"})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| ds = h.mustGet(doc) |
| want := map[string]interface{}{ |
| "str": "change", |
| "x": "2", |
| "y": "3", |
| } |
| if got := ds.Data(); !testEqual(got, want) { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| if !wr2.UpdateTime.Before(wr3.UpdateTime) { |
| t.Errorf("update time did not increase: old=%s, new=%s", wr2.UpdateTime, wr3.UpdateTime) |
| } |
| |
| // Another way to change only x and y is to pass a map with only |
| // those keys, and use MergeAll. |
| wr4, err := doc.Set(ctx, map[string]interface{}{"x": "4", "y": "5"}, MergeAll) |
| if err != nil { |
| t.Fatal(err) |
| } |
| ds = h.mustGet(doc) |
| want = map[string]interface{}{ |
| "str": "change", |
| "x": "4", |
| "y": "5", |
| } |
| if got := ds.Data(); !testEqual(got, want) { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| if !wr3.UpdateTime.Before(wr4.UpdateTime) { |
| t.Errorf("update time did not increase: old=%s, new=%s", wr3.UpdateTime, wr4.UpdateTime) |
| } |
| |
| // use firestore.Delete to delete a field. |
| _, err = doc.Set(ctx, map[string]interface{}{"str": Delete}, MergeAll) |
| ds = h.mustGet(doc) |
| want = map[string]interface{}{ |
| "x": "4", |
| "y": "5", |
| } |
| if got := ds.Data(); !testEqual(got, want) { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| |
| // Writing an empty doc with MergeAll should create the doc. |
| doc2 := coll.NewDoc() |
| want = map[string]interface{}{} |
| _, err = doc2.Set(ctx, want, MergeAll) |
| if err != nil { |
| t.Fatal(err) |
| } |
| ds = h.mustGet(doc2) |
| if got := ds.Data(); !testEqual(got, want) { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| } |
| |
| func TestIntegration_Delete(t *testing.T) { |
| ctx := context.Background() |
| doc := integrationColl(t).NewDoc() |
| h := testHelper{t} |
| h.mustCreate(doc, integrationTestMap) |
| wr := h.mustDelete(doc) |
| // Confirm that doc doesn't exist. |
| if _, err := doc.Get(ctx); grpc.Code(err) != codes.NotFound { |
| t.Fatalf("got error <%v>, want NotFound", err) |
| } |
| |
| er := func(_ *WriteResult, err error) error { return err } |
| |
| codeEq(t, "Delete on a missing doc", codes.OK, |
| er(doc.Delete(ctx))) |
| // TODO(jba): confirm that the server should return InvalidArgument instead of |
| // FailedPrecondition. |
| wr = h.mustCreate(doc, integrationTestMap) |
| codeEq(t, "Delete with wrong LastUpdateTime", codes.FailedPrecondition, |
| er(doc.Delete(ctx, LastUpdateTime(wr.UpdateTime.Add(-time.Millisecond))))) |
| codeEq(t, "Delete with right LastUpdateTime", codes.OK, |
| er(doc.Delete(ctx, LastUpdateTime(wr.UpdateTime)))) |
| } |
| |
| func TestIntegration_Update(t *testing.T) { |
| ctx := context.Background() |
| doc := integrationColl(t).NewDoc() |
| h := testHelper{t} |
| |
| h.mustCreate(doc, integrationTestMap) |
| fpus := []Update{ |
| {Path: "bool", Value: false}, |
| {Path: "time", Value: 17}, |
| {FieldPath: []string{"*", "`"}, Value: 18}, |
| {Path: "null", Value: Delete}, |
| {Path: "noSuchField", Value: Delete}, // deleting a non-existent field is a no-op |
| } |
| wr := h.mustUpdate(doc, fpus) |
| ds := h.mustGet(doc) |
| got := ds.Data() |
| want := copyMap(wantIntegrationTestMap) |
| want["bool"] = false |
| want["time"] = int64(17) |
| want["*"] = map[string]interface{}{"`": int64(18)} |
| delete(want, "null") |
| if !testEqual(got, want) { |
| t.Errorf("got\n%#v\nwant\n%#v", got, want) |
| } |
| |
| er := func(_ *WriteResult, err error) error { return err } |
| |
| codeEq(t, "Update on missing doc", codes.NotFound, |
| er(integrationColl(t).NewDoc().Update(ctx, fpus))) |
| codeEq(t, "Update with wrong LastUpdateTime", codes.FailedPrecondition, |
| er(doc.Update(ctx, fpus, LastUpdateTime(wr.UpdateTime.Add(-time.Millisecond))))) |
| codeEq(t, "Update with right LastUpdateTime", codes.OK, |
| er(doc.Update(ctx, fpus, LastUpdateTime(wr.UpdateTime)))) |
| } |
| |
| func TestIntegration_Collections(t *testing.T) { |
| ctx := context.Background() |
| c := integrationClient(t) |
| h := testHelper{t} |
| got, err := c.Collections(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| // There should be at least one collection. |
| if len(got) == 0 { |
| t.Error("got 0 top-level collections, want at least one") |
| } |
| |
| doc := integrationColl(t).NewDoc() |
| got, err = doc.Collections(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(got) != 0 { |
| t.Errorf("got %d collections, want 0", len(got)) |
| } |
| var want []*CollectionRef |
| for i := 0; i < 3; i++ { |
| id := collectionIDs.New() |
| cr := doc.Collection(id) |
| want = append(want, cr) |
| h.mustCreate(cr.NewDoc(), integrationTestMap) |
| } |
| got, err = doc.Collections(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !testEqual(got, want) { |
| t.Errorf("got\n%#v\nwant\n%#v", got, want) |
| } |
| } |
| |
| func TestIntegration_ServerTimestamp(t *testing.T) { |
| type S struct { |
| A int |
| B time.Time |
| C time.Time `firestore:"C.C,serverTimestamp"` |
| D map[string]interface{} |
| E time.Time `firestore:",omitempty,serverTimestamp"` |
| } |
| data := S{ |
| A: 1, |
| B: aTime, |
| // C is unset, so will get the server timestamp. |
| D: map[string]interface{}{"x": ServerTimestamp}, |
| // E is unset, so will get the server timestamp. |
| } |
| h := testHelper{t} |
| doc := integrationColl(t).NewDoc() |
| // Bound times of the RPC, with some slack for clock skew. |
| start := time.Now() |
| h.mustCreate(doc, data) |
| end := time.Now() |
| ds := h.mustGet(doc) |
| var got S |
| if err := ds.DataTo(&got); err != nil { |
| t.Fatal(err) |
| } |
| if !testEqual(got.B, aTime) { |
| t.Errorf("B: got %s, want %s", got.B, aTime) |
| } |
| checkTimeBetween(t, got.C, start, end) |
| if g, w := got.D["x"], got.C; !testEqual(g, w) { |
| t.Errorf(`D["x"] = %s, want equal to C (%s)`, g, w) |
| } |
| if g, w := got.E, got.C; !testEqual(g, w) { |
| t.Errorf(`E = %s, want equal to C (%s)`, g, w) |
| } |
| } |
| |
| func TestIntegration_MergeServerTimestamp(t *testing.T) { |
| ctx := context.Background() |
| doc := integrationColl(t).NewDoc() |
| h := testHelper{t} |
| |
| // Create a doc with an ordinary field "a" and a ServerTimestamp field "b". |
| _, err := doc.Set(ctx, map[string]interface{}{ |
| "a": 1, |
| "b": ServerTimestamp}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| docSnap := h.mustGet(doc) |
| data1 := docSnap.Data() |
| // Merge with a document with a different value of "a". However, |
| // specify only "b" in the list of merge fields. |
| _, err = doc.Set(ctx, |
| map[string]interface{}{"a": 2, "b": ServerTimestamp}, |
| Merge([]string{"b"})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // The result should leave "a" unchanged, while "b" is updated. |
| docSnap = h.mustGet(doc) |
| data2 := docSnap.Data() |
| if got, want := data2["a"], data1["a"]; got != want { |
| t.Errorf("got %v, want %v", got, want) |
| } |
| t1 := data1["b"].(time.Time) |
| t2 := data2["b"].(time.Time) |
| if !t1.Before(t2) { |
| t.Errorf("got t1=%s, t2=%s; want t1 before t2", t1, t2) |
| } |
| } |
| |
| func TestIntegration_MergeNestedServerTimestamp(t *testing.T) { |
| ctx := context.Background() |
| doc := integrationColl(t).NewDoc() |
| h := testHelper{t} |
| |
| // Create a doc with an ordinary field "a" a ServerTimestamp field "b", |
| // and a second ServerTimestamp field "c.d". |
| _, err := doc.Set(ctx, map[string]interface{}{ |
| "a": 1, |
| "b": ServerTimestamp, |
| "c": map[string]interface{}{"d": ServerTimestamp}, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| data1 := h.mustGet(doc).Data() |
| // Merge with a document with a different value of "a". However, |
| // specify only "c.d" in the list of merge fields. |
| _, err = doc.Set(ctx, |
| map[string]interface{}{ |
| "a": 2, |
| "b": ServerTimestamp, |
| "c": map[string]interface{}{"d": ServerTimestamp}, |
| }, |
| Merge([]string{"c", "d"})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // The result should leave "a" and "b" unchanged, while "c.d" is updated. |
| data2 := h.mustGet(doc).Data() |
| if got, want := data2["a"], data1["a"]; got != want { |
| t.Errorf("a: got %v, want %v", got, want) |
| } |
| want := data1["b"].(time.Time) |
| got := data2["b"].(time.Time) |
| if !got.Equal(want) { |
| t.Errorf("b: got %s, want %s", got, want) |
| } |
| t1 := data1["c"].(map[string]interface{})["d"].(time.Time) |
| t2 := data2["c"].(map[string]interface{})["d"].(time.Time) |
| if !t1.Before(t2) { |
| t.Errorf("got t1=%s, t2=%s; want t1 before t2", t1, t2) |
| } |
| } |
| |
| func TestIntegration_WriteBatch(t *testing.T) { |
| ctx := context.Background() |
| b := integrationClient(t).Batch() |
| h := testHelper{t} |
| doc1 := iColl.NewDoc() |
| doc2 := iColl.NewDoc() |
| b.Create(doc1, integrationTestMap) |
| b.Set(doc2, integrationTestMap) |
| b.Update(doc1, []Update{{Path: "bool", Value: false}}) |
| b.Update(doc1, []Update{{Path: "str", Value: Delete}}) |
| |
| wrs, err := b.Commit(ctx) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := len(wrs), 4; got != want { |
| t.Fatalf("got %d WriteResults, want %d", got, want) |
| } |
| got1 := h.mustGet(doc1).Data() |
| want := copyMap(wantIntegrationTestMap) |
| want["bool"] = false |
| delete(want, "str") |
| if !testEqual(got1, want) { |
| t.Errorf("got\n%#v\nwant\n%#v", got1, want) |
| } |
| got2 := h.mustGet(doc2).Data() |
| if !testEqual(got2, wantIntegrationTestMap) { |
| t.Errorf("got\n%#v\nwant\n%#v", got2, wantIntegrationTestMap) |
| } |
| // TODO(jba): test two updates to the same document when it is supported. |
| // TODO(jba): test verify when it is supported. |
| } |
| |
| func TestIntegration_Query(t *testing.T) { |
| ctx := context.Background() |
| coll := integrationColl(t) |
| h := testHelper{t} |
| var docs []*DocumentRef |
| var wants []map[string]interface{} |
| for i := 0; i < 3; i++ { |
| doc := coll.NewDoc() |
| docs = append(docs, doc) |
| // To support running this test in parallel with the others, use a field name |
| // that we don't use anywhere else. |
| h.mustCreate(doc, map[string]interface{}{"q": i, "x": 1}) |
| wants = append(wants, map[string]interface{}{"q": int64(i)}) |
| } |
| q := coll.Select("q").OrderBy("q", Asc) |
| for i, test := range []struct { |
| q Query |
| want []map[string]interface{} |
| }{ |
| {q, wants}, |
| {q.Where("q", ">", 1), wants[2:]}, |
| {q.WherePath([]string{"q"}, ">", 1), wants[2:]}, |
| {q.Offset(1).Limit(1), wants[1:2]}, |
| {q.StartAt(1), wants[1:]}, |
| {q.StartAfter(1), wants[2:]}, |
| {q.EndAt(1), wants[:2]}, |
| {q.EndBefore(1), wants[:1]}, |
| } { |
| gotDocs, err := test.q.Documents(ctx).GetAll() |
| if err != nil { |
| t.Errorf("#%d: %+v: %v", i, test.q, err) |
| continue |
| } |
| if len(gotDocs) != len(test.want) { |
| t.Errorf("#%d: %+v: got %d docs, want %d", i, test.q, len(gotDocs), len(test.want)) |
| continue |
| } |
| for j, g := range gotDocs { |
| if got, want := g.Data(), test.want[j]; !testEqual(got, want) { |
| t.Errorf("#%d: %+v, #%d: got\n%+v\nwant\n%+v", i, test.q, j, got, want) |
| } |
| } |
| } |
| _, err := coll.Select("q").Where("x", "==", 1).OrderBy("q", Asc).Documents(ctx).GetAll() |
| codeEq(t, "Where and OrderBy on different fields without an index", codes.FailedPrecondition, err) |
| |
| // Using the collection itself as the query should return the full documents. |
| allDocs, err := coll.Documents(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| seen := map[int64]bool{} // "q" values we see |
| for _, d := range allDocs { |
| data := d.Data() |
| q, ok := data["q"] |
| if !ok { |
| // A document from another test. |
| continue |
| } |
| if seen[q.(int64)] { |
| t.Errorf("%v: duplicate doc", data) |
| } |
| seen[q.(int64)] = true |
| if data["x"] != int64(1) { |
| t.Errorf("%v: wrong or missing 'x'", data) |
| } |
| if len(data) != 2 { |
| t.Errorf("%v: want two keys", data) |
| } |
| } |
| if got, want := len(seen), len(wants); got != want { |
| t.Errorf("got %d docs with 'q', want %d", len(seen), len(wants)) |
| } |
| } |
| |
| // Test unary filters. |
| func TestIntegration_QueryUnary(t *testing.T) { |
| ctx := context.Background() |
| coll := integrationColl(t) |
| h := testHelper{t} |
| h.mustCreate(coll.NewDoc(), map[string]interface{}{"x": 2, "q": "a"}) |
| h.mustCreate(coll.NewDoc(), map[string]interface{}{"x": 2, "q": nil}) |
| h.mustCreate(coll.NewDoc(), map[string]interface{}{"x": 2, "q": math.NaN()}) |
| wantNull := map[string]interface{}{"q": nil} |
| wantNaN := map[string]interface{}{"q": math.NaN()} |
| |
| base := coll.Select("q").Where("x", "==", 2) |
| for _, test := range []struct { |
| q Query |
| want map[string]interface{} |
| }{ |
| {base.Where("q", "==", nil), wantNull}, |
| {base.Where("q", "==", math.NaN()), wantNaN}, |
| } { |
| got, err := test.q.Documents(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(got) != 1 { |
| t.Errorf("got %d responses, want 1", len(got)) |
| continue |
| } |
| if g, w := got[0].Data(), test.want; !testEqual(g, w) { |
| t.Errorf("%v: got %v, want %v", test.q, g, w) |
| } |
| } |
| } |
| |
| // Test the special DocumentID field in queries. |
| func TestIntegration_QueryName(t *testing.T) { |
| ctx := context.Background() |
| h := testHelper{t} |
| |
| checkIDs := func(q Query, wantIDs []string) { |
| gots, err := q.Documents(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(gots) != len(wantIDs) { |
| t.Fatalf("got %d, want %d", len(gots), len(wantIDs)) |
| } |
| for i, g := range gots { |
| if got, want := g.Ref.ID, wantIDs[i]; got != want { |
| t.Errorf("#%d: got %s, want %s", i, got, want) |
| } |
| } |
| } |
| |
| coll := integrationColl(t) |
| var wantIDs []string |
| for i := 0; i < 3; i++ { |
| doc := coll.NewDoc() |
| h.mustCreate(doc, map[string]interface{}{"nm": 1}) |
| wantIDs = append(wantIDs, doc.ID) |
| } |
| sort.Strings(wantIDs) |
| q := coll.Where("nm", "==", 1).OrderBy(DocumentID, Asc) |
| checkIDs(q, wantIDs) |
| |
| // Empty Select. |
| q = coll.Select().Where("nm", "==", 1).OrderBy(DocumentID, Asc) |
| checkIDs(q, wantIDs) |
| |
| // Test cursors with __name__. |
| checkIDs(q.StartAt(wantIDs[1]), wantIDs[1:]) |
| checkIDs(q.EndAt(wantIDs[1]), wantIDs[:2]) |
| } |
| |
| func TestIntegration_QueryNested(t *testing.T) { |
| ctx := context.Background() |
| h := testHelper{t} |
| coll1 := integrationColl(t) |
| doc1 := coll1.NewDoc() |
| coll2 := doc1.Collection(collectionIDs.New()) |
| doc2 := coll2.NewDoc() |
| wantData := map[string]interface{}{"x": int64(1)} |
| h.mustCreate(doc2, wantData) |
| q := coll2.Select("x") |
| got, err := q.Documents(ctx).GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(got) != 1 { |
| t.Fatalf("got %d docs, want 1", len(got)) |
| } |
| if gotData := got[0].Data(); !testEqual(gotData, wantData) { |
| t.Errorf("got\n%+v\nwant\n%+v", gotData, wantData) |
| } |
| } |
| |
| func TestIntegration_RunTransaction(t *testing.T) { |
| ctx := context.Background() |
| h := testHelper{t} |
| |
| type Player struct { |
| Name string |
| Score int |
| Star bool `firestore:"*"` |
| } |
| |
| pat := Player{Name: "Pat", Score: 3, Star: false} |
| client := integrationClient(t) |
| patDoc := iColl.Doc("pat") |
| var anError error |
| incPat := func(_ context.Context, tx *Transaction) error { |
| doc, err := tx.Get(patDoc) |
| if err != nil { |
| return err |
| } |
| score, err := doc.DataAt("Score") |
| if err != nil { |
| return err |
| } |
| // Since the Star field is called "*", we must use DataAtPath to get it. |
| star, err := doc.DataAtPath([]string{"*"}) |
| if err != nil { |
| return err |
| } |
| err = tx.Update(patDoc, []Update{{Path: "Score", Value: int(score.(int64) + 7)}}) |
| if err != nil { |
| return err |
| } |
| // Since the Star field is called "*", we must use Update to change it. |
| err = tx.Update(patDoc, |
| []Update{{FieldPath: []string{"*"}, Value: !star.(bool)}}) |
| if err != nil { |
| return err |
| } |
| return anError |
| } |
| |
| h.mustCreate(patDoc, pat) |
| err := client.RunTransaction(ctx, incPat) |
| if err != nil { |
| t.Fatal(err) |
| } |
| ds := h.mustGet(patDoc) |
| var got Player |
| if err := ds.DataTo(&got); err != nil { |
| t.Fatal(err) |
| } |
| want := Player{Name: "Pat", Score: 10, Star: true} |
| if got != want { |
| t.Errorf("got %+v, want %+v", got, want) |
| } |
| |
| // Function returns error, so transaction is rolled back and no writes happen. |
| anError = errors.New("bad") |
| err = client.RunTransaction(ctx, incPat) |
| if err != anError { |
| t.Fatalf("got %v, want %v", err, anError) |
| } |
| if err := ds.DataTo(&got); err != nil { |
| t.Fatal(err) |
| } |
| // want is same as before. |
| if got != want { |
| t.Errorf("got %+v, want %+v", got, want) |
| } |
| } |
| |
| func TestIntegration_TransactionGetAll(t *testing.T) { |
| ctx := context.Background() |
| h := testHelper{t} |
| type Player struct { |
| Name string |
| Score int |
| } |
| lee := Player{Name: "Lee", Score: 3} |
| sam := Player{Name: "Sam", Score: 1} |
| client := integrationClient(t) |
| leeDoc := iColl.Doc("lee") |
| samDoc := iColl.Doc("sam") |
| h.mustCreate(leeDoc, lee) |
| h.mustCreate(samDoc, sam) |
| |
| err := client.RunTransaction(ctx, func(_ context.Context, tx *Transaction) error { |
| docs, err := tx.GetAll([]*DocumentRef{samDoc, leeDoc}) |
| if err != nil { |
| return err |
| } |
| for i, want := range []Player{sam, lee} { |
| var got Player |
| if err := docs[i].DataTo(&got); err != nil { |
| return err |
| } |
| if !testutil.Equal(got, want) { |
| return fmt.Errorf("got %+v, want %+v", got, want) |
| } |
| } |
| return nil |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func TestIntegration_WatchDocument(t *testing.T) { |
| coll := integrationColl(t) |
| ctx := context.Background() |
| h := testHelper{t} |
| doc := coll.NewDoc() |
| it := doc.Snapshots(ctx) |
| defer it.Stop() |
| |
| next := func() *DocumentSnapshot { |
| snap, err := it.Next() |
| if err != nil { |
| t.Fatal(err) |
| } |
| return snap |
| } |
| |
| snap := next() |
| if snap.Exists() { |
| t.Fatal("snapshot exists; it should not") |
| } |
| want := map[string]interface{}{"a": int64(1), "b": "two"} |
| h.mustCreate(doc, want) |
| snap = next() |
| if got := snap.Data(); !testutil.Equal(got, want) { |
| t.Fatalf("got %v, want %v", got, want) |
| } |
| |
| h.mustUpdate(doc, []Update{{Path: "a", Value: int64(2)}}) |
| want["a"] = int64(2) |
| snap = next() |
| if got := snap.Data(); !testutil.Equal(got, want) { |
| t.Fatalf("got %v, want %v", got, want) |
| } |
| |
| h.mustDelete(doc) |
| snap = next() |
| if snap.Exists() { |
| t.Fatal("snapshot exists; it should not") |
| } |
| |
| h.mustCreate(doc, want) |
| snap = next() |
| if got := snap.Data(); !testutil.Equal(got, want) { |
| t.Fatalf("got %v, want %v", got, want) |
| } |
| } |
| |
| type imap map[string]interface{} |
| |
| func TestIntegration_WatchQuery(t *testing.T) { |
| ctx := context.Background() |
| coll := integrationColl(t) |
| h := testHelper{t} |
| |
| q := coll.Where("e", ">", 1).OrderBy("e", Asc) |
| it := q.Snapshots(ctx) |
| defer it.Stop() |
| |
| next := func() ([]*DocumentSnapshot, []DocumentChange) { |
| diter, err := it.Next() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if it.ReadTime.IsZero() { |
| t.Fatal("zero time") |
| } |
| ds, err := diter.GetAll() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if it.Size != len(ds) { |
| t.Fatalf("Size=%d but we have %d docs", it.Size, len(ds)) |
| } |
| return ds, it.Changes |
| } |
| |
| copts := append([]cmp.Option{cmpopts.IgnoreFields(DocumentSnapshot{}, "ReadTime")}, cmpOpts...) |
| check := func(msg string, wantd []*DocumentSnapshot, wantc []DocumentChange) { |
| gotd, gotc := next() |
| if diff := testutil.Diff(gotd, wantd, copts...); diff != "" { |
| t.Errorf("%s: %s", msg, diff) |
| } |
| if diff := testutil.Diff(gotc, wantc, copts...); diff != "" { |
| t.Errorf("%s: %s", msg, diff) |
| } |
| } |
| |
| check("initial", nil, nil) |
| doc1 := coll.NewDoc() |
| h.mustCreate(doc1, imap{"e": int64(2), "b": "two"}) |
| wds := h.mustGet(doc1) |
| check("one", |
| []*DocumentSnapshot{wds}, |
| []DocumentChange{{Kind: DocumentAdded, Doc: wds, OldIndex: -1, NewIndex: 0}}) |
| |
| // Add a doc that does not match. We won't see a snapshot for this. |
| doc2 := coll.NewDoc() |
| h.mustCreate(doc2, imap{"e": int64(1)}) |
| |
| // Update the first doc. We should see the change. We won't see doc2. |
| h.mustUpdate(doc1, []Update{{Path: "e", Value: int64(3)}}) |
| wds = h.mustGet(doc1) |
| check("update", |
| []*DocumentSnapshot{wds}, |
| []DocumentChange{{Kind: DocumentModified, Doc: wds, OldIndex: 0, NewIndex: 0}}) |
| |
| // Now update doc so that it is not in the query. We should see a snapshot with no docs. |
| h.mustUpdate(doc1, []Update{{Path: "e", Value: int64(0)}}) |
| check("update2", nil, []DocumentChange{{Kind: DocumentRemoved, Doc: wds, OldIndex: 0, NewIndex: -1}}) |
| |
| // Add two docs out of order. We should see them in order. |
| doc3 := coll.NewDoc() |
| doc4 := coll.NewDoc() |
| want3 := imap{"e": int64(5)} |
| want4 := imap{"e": int64(4)} |
| h.mustCreate(doc3, want3) |
| h.mustCreate(doc4, want4) |
| wds4 := h.mustGet(doc4) |
| wds3 := h.mustGet(doc3) |
| check("two#1", |
| []*DocumentSnapshot{wds3}, |
| []DocumentChange{{Kind: DocumentAdded, Doc: wds3, OldIndex: -1, NewIndex: 0}}) |
| check("two#2", |
| []*DocumentSnapshot{wds4, wds3}, |
| []DocumentChange{{Kind: DocumentAdded, Doc: wds4, OldIndex: -1, NewIndex: 0}}) |
| // Delete a doc. |
| h.mustDelete(doc4) |
| check("after del", []*DocumentSnapshot{wds3}, []DocumentChange{{Kind: DocumentRemoved, Doc: wds4, OldIndex: 0, NewIndex: -1}}) |
| } |
| |
| func TestIntegration_WatchQueryCancel(t *testing.T) { |
| ctx := context.Background() |
| coll := integrationColl(t) |
| |
| q := coll.Where("e", ">", 1).OrderBy("e", Asc) |
| ctx, cancel := context.WithCancel(ctx) |
| it := q.Snapshots(ctx) |
| defer it.Stop() |
| |
| // First call opens the stream. |
| _, err := it.Next() |
| if err != nil { |
| t.Fatal(err) |
| } |
| cancel() |
| _, err = it.Next() |
| codeEq(t, "after cancel", codes.Canceled, err) |
| } |
| |
| func codeEq(t *testing.T, msg string, code codes.Code, err error) { |
| if grpc.Code(err) != code { |
| t.Fatalf("%s:\ngot <%v>\nwant code %s", msg, err, code) |
| } |
| } |
| |
| func loc() string { |
| _, file, line, ok := runtime.Caller(2) |
| if !ok { |
| return "???" |
| } |
| return fmt.Sprintf("%s:%d", filepath.Base(file), line) |
| } |
| |
| func copyMap(m map[string]interface{}) map[string]interface{} { |
| c := map[string]interface{}{} |
| for k, v := range m { |
| c[k] = v |
| } |
| return c |
| } |
| |
| func checkTimeBetween(t *testing.T, got, low, high time.Time) { |
| // Allow slack for clock skew. |
| const slack = 4 * time.Second |
| low = low.Add(-slack) |
| high = high.Add(slack) |
| if got.Before(low) || got.After(high) { |
| t.Fatalf("got %s, not in [%s, %s]", got, low, high) |
| } |
| } |
| |
| type testHelper struct { |
| t *testing.T |
| } |
| |
| func (h testHelper) mustCreate(doc *DocumentRef, data interface{}) *WriteResult { |
| wr, err := doc.Create(context.Background(), data) |
| if err != nil { |
| h.t.Fatalf("%s: creating: %v", loc(), err) |
| } |
| return wr |
| } |
| |
| func (h testHelper) mustUpdate(doc *DocumentRef, updates []Update) *WriteResult { |
| wr, err := doc.Update(context.Background(), updates) |
| if err != nil { |
| h.t.Fatalf("%s: updating: %v", loc(), err) |
| } |
| return wr |
| } |
| |
| func (h testHelper) mustGet(doc *DocumentRef) *DocumentSnapshot { |
| d, err := doc.Get(context.Background()) |
| if err != nil { |
| h.t.Fatalf("%s: getting: %v", loc(), err) |
| } |
| return d |
| } |
| |
| func (h testHelper) mustDelete(doc *DocumentRef) *WriteResult { |
| wr, err := doc.Delete(context.Background()) |
| if err != nil { |
| h.t.Fatalf("%s: updating: %v", loc(), err) |
| } |
| return wr |
| } |