go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/cloud/datastore_test.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cloud
    16  
    17  import (
    18  	"context"
    19  	"crypto/rand"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"os"
    23  	"testing"
    24  	"time"
    25  
    26  	"cloud.google.com/go/datastore"
    27  
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/errors"
    30  
    31  	ds "go.chromium.org/luci/gae/service/datastore"
    32  	"go.chromium.org/luci/gae/service/info"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  )
    36  
    37  func mkProperties(index bool, forceMulti bool, vals ...any) ds.PropertyData {
    38  	indexSetting := ds.ShouldIndex
    39  	if !index {
    40  		indexSetting = ds.NoIndex
    41  	}
    42  
    43  	if len(vals) == 1 && !forceMulti {
    44  		var prop ds.Property
    45  		prop.SetValue(vals[0], indexSetting)
    46  		return prop
    47  	}
    48  
    49  	result := make(ds.PropertySlice, len(vals))
    50  	for i, v := range vals {
    51  		result[i].SetValue(v, indexSetting)
    52  	}
    53  	return result
    54  }
    55  
    56  func mkp(vals ...any) ds.PropertyData   { return mkProperties(true, false, vals...) }
    57  func mkpNI(vals ...any) ds.PropertyData { return mkProperties(false, false, vals...) }
    58  
    59  // shouldBeUntypedNil asserts that actual is nil, with a nil type pointer
    60  // For example: https://play.golang.org/p/qN25iAYQw5Z
    61  func shouldBeUntypedNil(actual any, _ ...any) string {
    62  	if actual == nil {
    63  		return ""
    64  	}
    65  	return fmt.Sprintf(`Expected: (%T, %v)
    66  Actual:   (%T, %v)`, nil, nil, actual, actual)
    67  }
    68  
    69  func TestBoundDatastore(t *testing.T) {
    70  	t.Parallel()
    71  
    72  	Convey("boundDatastore", t, func() {
    73  		kc := ds.KeyContext{
    74  			AppID:     "app-id",
    75  			Namespace: "ns",
    76  		}
    77  
    78  		Convey("*datastore.Entity", func() {
    79  			ent := &datastore.Entity{
    80  				Key: &datastore.Key{
    81  					ID:        1,
    82  					Kind:      "Kind",
    83  					Namespace: "ns",
    84  					Parent: &datastore.Key{
    85  						Name:      "p",
    86  						Kind:      "Parent",
    87  						Namespace: "ns",
    88  					},
    89  				},
    90  				Properties: []datastore.Property{
    91  					{
    92  						Name:  "bool",
    93  						Value: true,
    94  					},
    95  					{
    96  						Name: "entity",
    97  						Value: &datastore.Entity{
    98  							Properties: []datastore.Property{
    99  								{
   100  									Name:    "[]byte",
   101  									NoIndex: true,
   102  									Value:   []byte("byte"),
   103  								},
   104  								{
   105  									Name:    "[]interface",
   106  									NoIndex: true,
   107  									Value:   []any{"interface"},
   108  								},
   109  								{
   110  									Name:    "geopoint",
   111  									NoIndex: true,
   112  									Value: datastore.GeoPoint{
   113  										Lat: 1,
   114  										Lng: 1,
   115  									},
   116  								},
   117  								{
   118  									Name:  "indexed",
   119  									Value: "hi",
   120  								},
   121  							},
   122  						},
   123  					},
   124  					{
   125  						Name:  "float64",
   126  						Value: 1.0,
   127  					},
   128  					{
   129  						Name:  "int64",
   130  						Value: int64(1),
   131  					},
   132  					{
   133  						Name: "key",
   134  						Value: &datastore.Key{
   135  							ID:        2,
   136  							Kind:      "kind",
   137  							Namespace: "ns",
   138  						},
   139  					},
   140  					{
   141  						Name:  "string",
   142  						Value: "string",
   143  					},
   144  					{
   145  						Name:  "time",
   146  						Value: ds.RoundTime(testclock.TestRecentTimeUTC),
   147  					},
   148  					{
   149  						Name:    "unindexed_entity",
   150  						NoIndex: true,
   151  						Value: &datastore.Entity{
   152  							Properties: []datastore.Property{
   153  								{
   154  									Name:    "field",
   155  									NoIndex: true,
   156  									Value:   "ho",
   157  								},
   158  							},
   159  						},
   160  					},
   161  				},
   162  			}
   163  
   164  			parent := kc.NewKey("Parent", "p", 0, nil)
   165  			key := kc.NewKey("Kind", "", 1, parent)
   166  
   167  			pm := ds.PropertyMap{
   168  				"$key":    ds.MkPropertyNI(key),
   169  				"$parent": ds.MkPropertyNI(parent),
   170  				"$kind":   ds.MkPropertyNI("Kind"),
   171  				"$id":     ds.MkPropertyNI(1),
   172  				"bool":    ds.MkProperty(true),
   173  				"entity": ds.MkProperty(ds.PropertyMap{
   174  					"[]byte": ds.MkPropertyNI([]byte("byte")),
   175  					"[]interface": ds.PropertySlice{
   176  						ds.MkPropertyNI("interface"),
   177  					},
   178  					"geopoint": ds.MkPropertyNI(ds.GeoPoint{Lat: 1, Lng: 1}),
   179  					"indexed":  ds.MkProperty("hi"),
   180  				}),
   181  				"unindexed_entity": ds.MkPropertyNI(ds.PropertyMap{
   182  					"field": ds.MkPropertyNI("ho"),
   183  				}),
   184  				"float64": ds.MkProperty(1.0),
   185  				"int64":   ds.MkProperty(int64(1)),
   186  				"key":     ds.MkProperty(kc.NewKey("kind", "", 2, nil)),
   187  				"string":  ds.MkProperty("string"),
   188  				"time":    ds.MkProperty(ds.RoundTime(testclock.TestRecentTimeUTC)),
   189  			}
   190  
   191  			Convey("gaeEntityToNative", func() {
   192  				So(gaeEntityToNative(kc, pm), ShouldResemble, ent)
   193  			})
   194  
   195  			Convey("nativeEntityToGAE", func() {
   196  				So(nativeEntityToGAE(kc, ent), ShouldResemble, pm)
   197  			})
   198  
   199  			Convey("gaeEntityToNative, nativeEntityToGAE", func() {
   200  				So(nativeEntityToGAE(kc, gaeEntityToNative(kc, pm)), ShouldResemble, pm)
   201  			})
   202  
   203  			Convey("nativeEntityToGAE, gaeEntityToNative", func() {
   204  				So(gaeEntityToNative(kc, nativeEntityToGAE(kc, ent)), ShouldResemble, ent)
   205  			})
   206  		})
   207  	})
   208  }
   209  
   210  // TestDatastore tests the cloud datastore implementation.
   211  //
   212  // Run the emulator:
   213  // $ gcloud beta emulators datastore start --use-firestore-in-datastore-mode
   214  //
   215  // Export the DATASTORE_EMULATOR_HOST environment variable, which the above
   216  // command printed.
   217  //
   218  // If the emulator environment is not detected, this test will be skipped.
   219  func TestDatastore(t *testing.T) {
   220  	t.Parallel()
   221  
   222  	// See if an emulator is running. If no emulator is running, we will skip this
   223  	// test suite.
   224  	emulatorHost := os.Getenv("DATASTORE_EMULATOR_HOST")
   225  	if emulatorHost == "" {
   226  		t.Skip("No emulator detected (DATASTORE_EMULATOR_HOST). Skipping test suite.")
   227  		return
   228  	}
   229  
   230  	Convey(fmt.Sprintf(`A cloud installation using datastore emulator %q`, emulatorHost), t, func() {
   231  		c := context.Background()
   232  		client, err := datastore.NewClient(c, "luci-gae-test")
   233  		So(err, ShouldBeNil)
   234  		defer client.Close()
   235  
   236  		testTime := ds.RoundTime(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC))
   237  		_ = testTime
   238  
   239  		cfg := ConfigLite{ProjectID: "luci-gae-test", DS: client}
   240  		c = cfg.Use(c)
   241  
   242  		Convey(`Supports namespaces`, func() {
   243  			namespaces := []string{"foo", "bar", "baz"}
   244  
   245  			// Clear all used entities from all namespaces.
   246  			for _, ns := range namespaces {
   247  				nsCtx := info.MustNamespace(c, ns)
   248  
   249  				keys := make([]*ds.Key, len(namespaces))
   250  				for i := range keys {
   251  					keys[i] = ds.MakeKey(nsCtx, "Test", i+1)
   252  				}
   253  				So(errors.Filter(ds.Delete(nsCtx, keys), ds.ErrNoSuchEntity), ShouldBeNil)
   254  			}
   255  
   256  			// Put one entity per namespace.
   257  			for i, ns := range namespaces {
   258  				nsCtx := info.MustNamespace(c, ns)
   259  
   260  				pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp(i + 1), "Value": mkp(i)}
   261  				So(ds.Put(nsCtx, pmap), ShouldBeNil)
   262  			}
   263  
   264  			// Make sure that entity only exists in that namespace.
   265  			for _, ns := range namespaces {
   266  				nsCtx := info.MustNamespace(c, ns)
   267  
   268  				for i := range namespaces {
   269  					pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp(i + 1)}
   270  					err := ds.Get(nsCtx, pmap)
   271  
   272  					if namespaces[i] == ns {
   273  						So(err, ShouldBeNil)
   274  					} else {
   275  						So(err, ShouldEqual, ds.ErrNoSuchEntity)
   276  					}
   277  				}
   278  			}
   279  		})
   280  
   281  		Convey(`In a clean random testing namespace`, func() {
   282  			// Enter a namespace for this round of tests.
   283  			randNamespace := make([]byte, 32)
   284  			if _, err := rand.Read(randNamespace); err != nil {
   285  				panic(err)
   286  			}
   287  			c = info.MustNamespace(c, fmt.Sprintf("testing-%s", hex.EncodeToString(randNamespace)))
   288  
   289  			// Execute a kindless query to clear the namespace.
   290  			q := ds.NewQuery("").KeysOnly(true)
   291  			var allKeys []*ds.Key
   292  			So(ds.GetAll(c, q, &allKeys), ShouldBeNil)
   293  			So(ds.Delete(c, allKeys), ShouldBeNil)
   294  
   295  			Convey(`Can allocate an ID range`, func() {
   296  				var keys []*ds.Key
   297  				keys = append(keys, ds.NewIncompleteKeys(c, 10, "Bar", ds.MakeKey(c, "Foo", 12))...)
   298  				keys = append(keys, ds.NewIncompleteKeys(c, 10, "Baz", ds.MakeKey(c, "Foo", 12))...)
   299  
   300  				seen := map[string]struct{}{}
   301  				So(ds.AllocateIDs(c, keys), ShouldBeNil)
   302  				for _, k := range keys {
   303  					So(k.IsIncomplete(), ShouldBeFalse)
   304  					seen[k.String()] = struct{}{}
   305  				}
   306  
   307  				So(ds.AllocateIDs(c, keys), ShouldBeNil)
   308  				for _, k := range keys {
   309  					So(k.IsIncomplete(), ShouldBeFalse)
   310  
   311  					_, ok := seen[k.String()]
   312  					So(ok, ShouldBeFalse)
   313  				}
   314  			})
   315  
   316  			Convey(`Can get, put, and delete entities`, func() {
   317  				// Put: "foo", "bar", "baz".
   318  				put := []ds.PropertyMap{
   319  					{"$kind": mkp("test"), "$id": mkp("foo"), "Value": mkp(1337)},
   320  					{"$kind": mkp("test"), "$id": mkp("bar"), "Value": mkp(42)},
   321  					{"$kind": mkp("test"), "$id": mkp("baz"), "Value": mkp(0xd065)},
   322  				}
   323  				So(ds.Put(c, put), ShouldBeNil)
   324  
   325  				// Delete: "bar".
   326  				So(ds.Delete(c, ds.MakeKey(c, "test", "bar")), ShouldBeNil)
   327  
   328  				// Get: "foo", "bar", "baz"
   329  				get := []ds.PropertyMap{
   330  					{"$kind": mkp("test"), "$id": mkp("foo")},
   331  					{"$kind": mkp("test"), "$id": mkp("bar")},
   332  					{"$kind": mkp("test"), "$id": mkp("baz")},
   333  				}
   334  
   335  				err := ds.Get(c, get)
   336  				So(err, ShouldHaveSameTypeAs, errors.MultiError(nil))
   337  
   338  				merr := err.(errors.MultiError)
   339  				So(len(merr), ShouldEqual, 3)
   340  				So(merr[0], ShouldBeNil)
   341  				So(merr[1], ShouldEqual, ds.ErrNoSuchEntity)
   342  				So(merr[2], ShouldBeNil)
   343  
   344  				// put[1] will not be retrieved (delete)
   345  				put[1] = get[1]
   346  				So(get, ShouldResemble, put)
   347  			})
   348  
   349  			Convey(`Can put and get all supported entity fields.`, func() {
   350  				put := ds.PropertyMap{
   351  					"$id":   mkpNI("foo"),
   352  					"$kind": mkpNI("FooType"),
   353  
   354  					"Number":    mkp(1337),
   355  					"String":    mkpNI("hello"),
   356  					"Bytes":     mkp([]byte("world")),
   357  					"Time":      mkp(testTime),
   358  					"Float":     mkpNI(3.14),
   359  					"Key":       mkp(ds.MakeKey(c, "Parent", "ParentID", "Child", 1337)),
   360  					"Null":      mkp(nil),
   361  					"NullSlice": mkp(nil, nil),
   362  
   363  					"ComplexSlice": mkp(1337, "string", []byte("bytes"), testTime, float32(3.14),
   364  						float64(2.71), true, nil, ds.MakeKey(c, "SomeKey", "SomeID")),
   365  
   366  					"Single":      mkp("single"),
   367  					"SingleSlice": mkProperties(true, true, "single"), // Force a single "multi" value.
   368  					"EmptySlice":  ds.PropertySlice(nil),
   369  
   370  					"SingleEntity": mkp(ds.PropertyMap{
   371  						"$id":     mkpNI("inner"),
   372  						"$kind":   mkpNI("Inner"),
   373  						"$key":    mkpNI(ds.MakeKey(c, "Inner", "inner")),
   374  						"$parent": mkpNI(nil),
   375  						"prop":    mkp(1),
   376  						"deeper":  mkp(ds.PropertyMap{"deep": mkp(123)}),
   377  					}),
   378  					"SliceOfEntities": mkp(
   379  						ds.PropertyMap{"prop": mkp(2)},
   380  						ds.PropertyMap{"prop": mkp(3)},
   381  					),
   382  				}
   383  				So(ds.Put(c, put), ShouldBeNil)
   384  
   385  				get := ds.PropertyMap{
   386  					"$id":   mkpNI("foo"),
   387  					"$kind": mkpNI("FooType"),
   388  				}
   389  				So(ds.Get(c, get), ShouldBeNil)
   390  				So(get, ShouldResemble, put)
   391  			})
   392  
   393  			Convey(`Can Get empty []byte slice as nil`, func() {
   394  				put := ds.PropertyMap{
   395  					"$id":   mkpNI("foo"),
   396  					"$kind": mkpNI("FooType"),
   397  					"Empty": mkp([]byte(nil)),
   398  					"Nilly": mkp([]byte{}),
   399  				}
   400  				get := ds.PropertyMap{
   401  					"$id":   put["$id"],
   402  					"$kind": put["$kind"],
   403  				}
   404  				exp := put.Clone()
   405  				exp["Nilly"] = mkp([]byte(nil))
   406  
   407  				So(ds.Put(c, put), ShouldBeNil)
   408  				So(ds.Get(c, get), ShouldBeNil)
   409  				So(get, ShouldResemble, exp)
   410  			})
   411  
   412  			Convey(`With several entities installed`, func() {
   413  				So(ds.Put(c, []ds.PropertyMap{
   414  					{"$kind": mkp("Test"), "$id": mkp("foo"), "FooBar": mkp(true)},
   415  					{"$kind": mkp("Test"), "$id": mkp("bar"), "FooBar": mkp(true)},
   416  					{"$kind": mkp("Test"), "$id": mkp("baz")},
   417  					{"$kind": mkp("Test"), "$id": mkp("qux")},
   418  					{"$kind": mkp("Test"), "$id": mkp("quux"), "$parent": mkp(ds.MakeKey(c, "Test", "baz"))},
   419  					{"$kind": mkp("Test"), "$id": mkp("quuz"), "$parent": mkp(ds.MakeKey(c, "Test", "baz"))},
   420  					// Entities for checking IN query.
   421  					{"$kind": mkp("AAA"), "$id": mkp("e1"), "Slice": mkp("a", "b")},
   422  					{"$kind": mkp("AAA"), "$id": mkp("e2"), "Slice": mkp("a", "c")},
   423  				}), ShouldBeNil)
   424  
   425  				withAllMeta := func(pm ds.PropertyMap) ds.PropertyMap {
   426  					prop := pm["$key"].(ds.Property)
   427  					key := prop.Value().(*ds.Key)
   428  					pm["$id"] = mkpNI(key.StringID())
   429  					pm["$kind"] = mkpNI(key.Kind())
   430  					pm["$parent"] = mkpNI(key.Parent())
   431  					return pm
   432  				}
   433  
   434  				q := ds.NewQuery("Test")
   435  
   436  				Convey(`Can query for entities with FooBar == true.`, func() {
   437  					var results []ds.PropertyMap
   438  					q = q.Eq("FooBar", true)
   439  					So(ds.GetAll(c, q, &results), ShouldBeNil)
   440  
   441  					So(results, ShouldResemble, []ds.PropertyMap{
   442  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "bar")), "FooBar": mkp(true)}),
   443  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "foo")), "FooBar": mkp(true)}),
   444  					})
   445  				})
   446  
   447  				Convey(`Can query for entities whose __key__ > "baz".`, func() {
   448  					var results []ds.PropertyMap
   449  					q = q.Gt("__key__", ds.MakeKey(c, "Test", "baz"))
   450  					So(ds.GetAll(c, q, &results), ShouldBeNil)
   451  
   452  					So(results, ShouldResemble, []ds.PropertyMap{
   453  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quux"))}),
   454  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quuz"))}),
   455  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "foo")), "FooBar": mkp(true)}),
   456  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "qux"))}),
   457  					})
   458  				})
   459  
   460  				Convey(`Can query for entities whose ancestor is "baz".`, func() {
   461  					var results []ds.PropertyMap
   462  					q := ds.NewQuery("Test").Ancestor(ds.MakeKey(c, "Test", "baz"))
   463  					So(ds.GetAll(c, q, &results), ShouldBeNil)
   464  
   465  					So(results, ShouldResemble, []ds.PropertyMap{
   466  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz"))}),
   467  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quux"))}),
   468  						withAllMeta(ds.PropertyMap{"$key": mkpNI(ds.MakeKey(c, "Test", "baz", "Test", "quuz"))}),
   469  					})
   470  				})
   471  
   472  				// TODO(vadimsh): Unfortunately Cloud Datastore emulator doesn't
   473  				// support IN queries, see https://cloud.google.com/datastore/docs/tools/datastore-emulator#known_issues
   474  				SkipConvey(`Can use IN in queries`, func() {
   475  					var results []*ds.Key
   476  					q := ds.NewQuery("AAA").In("Slice", "b", "c").KeysOnly(true)
   477  					So(ds.GetAll(c, q, &results), ShouldBeNil)
   478  					So(results, ShouldResemble, []*ds.Key{
   479  						ds.MakeKey(c, "AAA", "e1"),
   480  						ds.MakeKey(c, "AAA", "e2"),
   481  					})
   482  				})
   483  
   484  				Convey(`Can transactionally get and put.`, func() {
   485  					err := ds.RunInTransaction(c, func(c context.Context) error {
   486  						pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("qux")}
   487  						if err := ds.Get(c, pmap); err != nil {
   488  							return err
   489  						}
   490  
   491  						pmap["ExtraField"] = mkp("Present!")
   492  						return ds.Put(c, pmap)
   493  					}, nil)
   494  					So(err, ShouldBeNil)
   495  
   496  					pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("qux")}
   497  					err = ds.RunInTransaction(c, func(c context.Context) error {
   498  						return ds.Get(c, pmap)
   499  					}, nil)
   500  					So(err, ShouldBeNil)
   501  					So(pmap, ShouldResemble, ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("qux"), "ExtraField": mkp("Present!")})
   502  				})
   503  
   504  				Convey(`Can fail in a transaction with no effect.`, func() {
   505  					testError := errors.New("test error")
   506  
   507  					noTxnPM := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("no txn")}
   508  					err := ds.RunInTransaction(c, func(c context.Context) error {
   509  						So(ds.CurrentTransaction(c), ShouldNotBeNil)
   510  
   511  						pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("quux")}
   512  						if err := ds.Put(c, pmap); err != nil {
   513  							return err
   514  						}
   515  
   516  						// Put an entity outside of the transaction so we can confirm that
   517  						// it was added even when the transaction fails.
   518  						if err := ds.Put(ds.WithoutTransaction(c), noTxnPM); err != nil {
   519  							return err
   520  						}
   521  						return testError
   522  					}, nil)
   523  					So(err, ShouldEqual, testError)
   524  
   525  					// Confirm that noTxnPM was added.
   526  					So(ds.CurrentTransaction(c), shouldBeUntypedNil)
   527  					So(ds.Get(c, noTxnPM), ShouldBeNil)
   528  
   529  					pmap := ds.PropertyMap{"$kind": mkp("Test"), "$id": mkp("quux")}
   530  					err = ds.RunInTransaction(c, func(c context.Context) error {
   531  						return ds.Get(c, pmap)
   532  					}, nil)
   533  					So(err, ShouldEqual, ds.ErrNoSuchEntity)
   534  				})
   535  			})
   536  		})
   537  	})
   538  }