blob: b82f130376bb3bd9b10968b85f6973334fba1b2c [file] [log] [blame]
// 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"
"reflect"
"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/codes"
"google.golang.org/grpc/status"
)
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")
}
wantDBPath := "projects/" + testProjectID + "/databases/(default)"
ti := &testutil.HeadersEnforcer{
Checkers: []*testutil.HeaderChecker{
testutil.XGoogClientHeaderChecker,
{
Key: "google-cloud-resource-prefix",
ValuesValidator: func(values ...string) error {
if len(values) == 0 {
return errors.New("expected non-blank header")
}
if values[0] != wantDBPath {
return fmt.Errorf("resource prefix mismatch; got %q want %q", values[0], wantDBPath)
}
return nil
},
},
},
}
copts := append(ti.CallOptions(), option.WithTokenSource(ts))
c, err := NewClient(ctx, testProjectID, copts...)
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
}
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,
"int8": int8(2),
"int16": int16(3),
"int32": int32(4),
"int64": int64(5),
"uint8": uint8(6),
"uint16": uint16(7),
"uint32": uint32(8),
"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),
"int8": int64(2),
"int16": int64(3),
"int32": int64(4),
"int64": int64(5),
"uint8": int64(6),
"uint16": int64(7),
"uint32": int64(8),
"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.Error("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); status.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
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", "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
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)
}
// 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
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", "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)
}
}
func TestIntegration_Increment_Create(t *testing.T) {
doc := integrationColl(t).NewDoc()
h := testHelper{t}
path := "somePath"
want := 7
h.mustCreate(doc, map[string]interface{}{
path: Increment(want),
})
ds := h.mustGet(doc)
var gotMap map[string]int
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)
}
if gotMap[path] != want {
t.Fatalf("want %d, got %d", want, gotMap[path])
}
}
// Also checks that all appropriate types are supported.
func TestIntegration_Increment_Update(t *testing.T) {
type MyInt = int // Test a custom type.
for _, tc := range []struct {
// All three should be same type.
start interface{}
inc interface{}
want interface{}
wantErr bool
}{
{start: int(7), inc: int(4), want: int(11)},
{start: int8(7), inc: int8(4), want: int8(11)},
{start: int16(7), inc: int16(4), want: int16(11)},
{start: int32(7), inc: int32(4), want: int32(11)},
{start: int64(7), inc: int64(4), want: int64(11)},
{start: uint8(7), inc: uint8(4), want: uint8(11)},
{start: uint16(7), inc: uint16(4), want: uint16(11)},
{start: uint32(7), inc: uint32(4), want: uint32(11)},
{start: float32(7.7), inc: float32(4.1), want: float32(11.8)},
{start: float64(7.7), inc: float64(4.1), want: float64(11.8)},
{start: MyInt(7), inc: MyInt(4), want: MyInt(11)},
{start: 7, inc: "strings are not allowed", wantErr: true},
{start: 7, inc: uint(3), wantErr: true},
{start: 7, inc: uint64(3), wantErr: true},
} {
typeStr := reflect.TypeOf(tc.inc).String()
t.Run(typeStr, func(t *testing.T) {
doc := integrationColl(t).NewDoc()
h := testHelper{t}
path := "somePath"
h.mustCreate(doc, map[string]interface{}{
path: tc.start,
})
fpus := []Update{
{
Path: path,
Value: Increment(tc.inc),
},
}
_, err := doc.Update(context.Background(), fpus)
if err != nil {
if tc.wantErr {
return
}
h.t.Fatalf("%s: updating: %v", loc(), err)
}
ds := h.mustGet(doc)
var gotMap map[string]interface{}
if err := ds.DataTo(&gotMap); err != nil {
t.Fatal(err)
}
switch tc.want.(type) {
case int, int8, int16, int32, int64:
if _, ok := gotMap[path]; !ok {
t.Fatalf("expected a %v key in data, got %v", path, gotMap)
}
if got, want := reflect.ValueOf(gotMap[path]).Int(), reflect.ValueOf(tc.want).Int(); got != want {
t.Fatalf("want %v, got %v", want, got)
}
case uint8, uint16, uint32:
if _, ok := gotMap[path]; !ok {
t.Fatalf("expected a %v key in data, got %v", path, gotMap)
}
if got, want := uint64(reflect.ValueOf(gotMap[path]).Int()), reflect.ValueOf(tc.want).Uint(); got != want {
t.Fatalf("want %v, got %v", want, got)
}
case float32, float64:
if _, ok := gotMap[path]; !ok {
t.Fatalf("expected a %v key in data, got %v", path, gotMap)
}
const precision = 1e-6 // Floats are never precisely comparable.
if got, want := reflect.ValueOf(gotMap[path]).Float(), reflect.ValueOf(tc.want).Float(); math.Abs(got-want) > precision {
t.Fatalf("want %v, got %v", want, got)
}
default:
// Either some unsupported type was added without specifying
// wantErr, or a supported type needs to be added to this
// switch statement.
t.Fatalf("unsupported type %T", tc.want)
}
})
}
}
func TestIntegration_Increment_Set(t *testing.T) {
coll := integrationColl(t)
h := testHelper{t}
path := "somePath"
want := 9
doc := coll.NewDoc()
newData := map[string]interface{}{
path: Increment(want),
}
h.mustSet(doc, newData)
ds := h.mustGet(doc)
var gotMap map[string]int
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)
}
if gotMap[path] != want {
t.Fatalf("want %d, got %d", want, gotMap[path])
}
}
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 TestIntegration_CollectionGroupQueries(t *testing.T) {
shouldBeFoundID := collectionIDs.New()
shouldNotBeFoundID := collectionIDs.New()
ctx := context.Background()
h := testHelper{t}
client := integrationClient(t)
cr1 := client.Collection(shouldBeFoundID)
dr1 := cr1.Doc("should-be-found-1")
h.mustCreate(dr1, map[string]string{"some-key": "should-be-found"})
defer h.mustDelete(dr1)
dr1.Collection(shouldBeFoundID)
dr2 := cr1.Doc("should-be-found-2")
h.mustCreate(dr2, map[string]string{"some-key": "should-be-found"})
defer h.mustDelete(dr2)
cr3 := client.Collection(shouldNotBeFoundID)
dr3 := cr3.Doc("should-not-be-found")
h.mustCreate(dr3, map[string]string{"some-key": "should-NOT-be-found"})
defer h.mustDelete(dr3)
cg := client.CollectionGroup(shouldBeFoundID)
snaps, err := cg.Documents(ctx).GetAll()
if err != nil {
t.Fatal(err)
}
if len(snaps) != 2 {
t.Fatalf("expected 2 snapshots but got %d", len(snaps))
}
if snaps[0].Ref.ID != "should-be-found-1" {
t.Fatalf("expected ID 'should-be-found-1', got %s", snaps[0].Ref.ID)
}
if snaps[1].Ref.ID != "should-be-found-2" {
t.Fatalf("expected ID 'should-be-found-2', got %s", snaps[1].Ref.ID)
}
}
func codeEq(t *testing.T, msg string, code codes.Code, err error) {
if status.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
}
func TestDetectProjectID(t *testing.T) {
if testing.Short() {
t.Skip("Integration tests skipped in short mode")
}
ctx := context.Background()
creds := testutil.Credentials(ctx)
if creds == nil {
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
}
// Use creds with project ID.
if _, err := NewClient(ctx, DetectProjectID, option.WithCredentials(creds)); err != nil {
t.Errorf("NewClient: %v", err)
}
ts := testutil.ErroringTokenSource{}
// Try to use creds without project ID.
_, err := NewClient(ctx, DetectProjectID, option.WithTokenSource(ts))
if err == nil || err.Error() != "firestore: see the docs on DetectProjectID" {
t.Errorf("expected an error while using TokenSource that does not have a project ID")
}
}