|
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339 |
- // 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 (
- "context"
- "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"
- "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)
- h := testHelper{t}
- ctx := context.Background()
-
- // Set Should be able to create a new doc.
- doc := coll.NewDoc()
- wr1 := h.mustSet(doc, integrationTestMap)
- // Calling Set on the doc completely replaces the contents.
- // The update time should increase.
- newData := map[string]interface{}{
- "str": "change",
- "x": "1",
- }
- wr2 := h.mustSet(doc, newData)
- 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.
- // TODO(deklerk) We should be able to use mustSet, but then we get a test error. We should investigate this.
- _, err = doc.Set(ctx, map[string]interface{}{"str": Delete}, MergeAll)
- if err != nil {
- t.Fatal(err)
- }
- 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{}{}
- h.mustSet(doc2, want, MergeAll)
- 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)
- 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) {
- doc := integrationColl(t).NewDoc()
- h := testHelper{t}
-
- // Create a doc with an ordinary field "a" and a ServerTimestamp field "b".
- h.mustSet(doc, map[string]interface{}{"a": 1, "b": ServerTimestamp})
- 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.
- h.mustSet(doc, map[string]interface{}{"a": 2, "b": ServerTimestamp}, Merge([]string{"b"}))
- // 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) {
- 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".
- h.mustSet(doc, map[string]interface{}{
- "a": 1,
- "b": ServerTimestamp,
- "c": map[string]interface{}{"d": ServerTimestamp},
- })
- 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.
- h.mustSet(doc, map[string]interface{}{
- "a": 2,
- "b": ServerTimestamp,
- "c": map[string]interface{}{"d": ServerTimestamp},
- }, Merge([]string{"c", "d"}))
- // 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 wants []map[string]interface{}
- for i := 0; i < 3; i++ {
- doc := coll.NewDoc()
- // 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)
- }
- }
-
- func TestIntegration_ArrayUnion_Create(t *testing.T) {
- path := "somepath"
- data := map[string]interface{}{
- path: ArrayUnion("a", "b"),
- }
-
- doc := integrationColl(t).NewDoc()
- h := testHelper{t}
- h.mustCreate(doc, data)
- ds := h.mustGet(doc)
- var gotMap map[string][]string
- if err := ds.DataTo(&gotMap); err != nil {
- t.Fatal(err)
- }
- if _, ok := gotMap[path]; !ok {
- t.Fatalf("expected a %v key in data, got %v", path, gotMap)
- }
-
- want := []string{"a", "b"}
- for i, v := range gotMap[path] {
- if v != want[i] {
- t.Fatalf("got\n%#v\nwant\n%#v", gotMap[path], want)
- }
- }
- }
-
- func TestIntegration_ArrayUnion_Update(t *testing.T) {
- doc := integrationColl(t).NewDoc()
- h := testHelper{t}
- path := "somepath"
-
- h.mustCreate(doc, map[string]interface{}{
- path: []string{"a", "b"},
- })
- fpus := []Update{
- {
- Path: path,
- Value: ArrayUnion("this should be added"),
- },
- }
- h.mustUpdate(doc, fpus)
- ds := h.mustGet(doc)
- var gotMap map[string][]string
- err := ds.DataTo(&gotMap)
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := gotMap[path]; !ok {
- t.Fatalf("expected a %v key in data, got %v", path, gotMap)
- }
-
- want := []string{"a", "b", "this should be added"}
- for i, v := range gotMap[path] {
- if v != want[i] {
- t.Fatalf("got\n%#v\nwant\n%#v", gotMap[path], want)
- }
- }
- }
-
- func TestIntegration_ArrayUnion_Set(t *testing.T) {
- coll := integrationColl(t)
- h := testHelper{t}
- path := "somepath"
-
- doc := coll.NewDoc()
- newData := map[string]interface{}{
- path: ArrayUnion("a", "b"),
- }
- h.mustSet(doc, newData)
- ds := h.mustGet(doc)
- var gotMap map[string][]string
- if err := ds.DataTo(&gotMap); err != nil {
- t.Fatal(err)
- }
- if _, ok := gotMap[path]; !ok {
- t.Fatalf("expected a %v key in data, got %v", path, gotMap)
- }
-
- want := []string{"a", "b"}
- for i, v := range gotMap[path] {
- if v != want[i] {
- t.Fatalf("got\n%#v\nwant\n%#v", gotMap[path], want)
- }
- }
- }
-
- func TestIntegration_ArrayRemove_Create(t *testing.T) {
- doc := integrationColl(t).NewDoc()
- h := testHelper{t}
- path := "somepath"
-
- h.mustCreate(doc, map[string]interface{}{
- path: ArrayRemove("a", "b"),
- })
-
- ds := h.mustGet(doc)
- var gotMap map[string][]string
- err := ds.DataTo(&gotMap)
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := gotMap[path]; !ok {
- t.Fatalf("expected a %v key in data, got %v", path, gotMap)
- }
-
- // A create with arrayRemove results in an empty array.
- want := []string(nil)
- if !testEqual(gotMap[path], want) {
- t.Fatalf("got\n%#v\nwant\n%#v", gotMap[path], want)
- }
- }
-
- func TestIntegration_ArrayRemove_Update(t *testing.T) {
- doc := integrationColl(t).NewDoc()
- h := testHelper{t}
- path := "somepath"
-
- h.mustCreate(doc, map[string]interface{}{
- path: []string{"a", "this should be removed", "c"},
- })
- fpus := []Update{
- {
- Path: path,
- Value: ArrayRemove("this should be removed"),
- },
- }
- h.mustUpdate(doc, fpus)
- ds := h.mustGet(doc)
- var gotMap map[string][]string
- err := ds.DataTo(&gotMap)
- if err != nil {
- t.Fatal(err)
- }
- if _, ok := gotMap[path]; !ok {
- t.Fatalf("expected a %v key in data, got %v", path, gotMap)
- }
-
- want := []string{"a", "c"}
- for i, v := range gotMap[path] {
- if v != want[i] {
- t.Fatalf("got\n%#v\nwant\n%#v", gotMap[path], want)
- }
- }
- }
-
- func TestIntegration_ArrayRemove_Set(t *testing.T) {
- coll := integrationColl(t)
- h := testHelper{t}
- path := "somepath"
-
- doc := coll.NewDoc()
- newData := map[string]interface{}{
- path: ArrayRemove("a", "b"),
- }
- h.mustSet(doc, newData)
- ds := h.mustGet(doc)
- var gotMap map[string][]string
- if err := ds.DataTo(&gotMap); err != nil {
- t.Fatal(err)
- }
- if _, ok := gotMap[path]; !ok {
- t.Fatalf("expected a %v key in data, got %v", path, gotMap)
- }
-
- want := []string(nil)
- if !testEqual(gotMap[path], want) {
- t.Fatalf("got\n%#v\nwant\n%#v", gotMap[path], 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) {
- qsnap, err := it.Next()
- if err != nil {
- t.Fatal(err)
- }
- if qsnap.ReadTime.IsZero() {
- t.Fatal("zero time")
- }
- ds, err := qsnap.Documents.GetAll()
- if err != nil {
- t.Fatal(err)
- }
- if qsnap.Size != len(ds) {
- t.Fatalf("Size=%d but we have %d docs", qsnap.Size, len(ds))
- }
- return ds, qsnap.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 TestIntegration_MissingDocs(t *testing.T) {
- ctx := context.Background()
- h := testHelper{t}
- client := integrationClient(t)
- coll := client.Collection(collectionIDs.New())
- dr1 := coll.NewDoc()
- dr2 := coll.NewDoc()
- dr3 := dr2.Collection("sub").NewDoc()
- h.mustCreate(dr1, integrationTestMap)
- defer h.mustDelete(dr1)
- h.mustCreate(dr3, integrationTestMap)
- defer h.mustDelete(dr3)
-
- // dr1 is a document in coll. dr2 was never created, but there are documents in
- // its sub-collections. It is "missing".
- // The Collection.DocumentRefs method includes missing document refs.
- want := []string{dr1.Path, dr2.Path}
- drs, err := coll.DocumentRefs(ctx).GetAll()
- if err != nil {
- t.Fatal(err)
- }
- var got []string
- for _, dr := range drs {
- got = append(got, dr.Path)
- }
- sort.Strings(want)
- sort.Strings(got)
- if !testutil.Equal(got, want) {
- t.Errorf("got %v, want %v", got, want)
- }
- }
-
- 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
- }
-
- func (h testHelper) mustSet(doc *DocumentRef, data interface{}, opts ...SetOption) *WriteResult {
- wr, err := doc.Set(context.Background(), data, opts...)
- if err != nil {
- h.t.Fatalf("%s: updating: %v", loc(), err)
- }
- return wr
- }
|