go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/rpc/config_test.go (about)

     1  // Copyright 2018 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 rpc
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  
    21  	"google.golang.org/genproto/protobuf/field_mask"
    22  	"google.golang.org/protobuf/types/known/emptypb"
    23  
    24  	"go.chromium.org/luci/gae/impl/memory"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  	"go.chromium.org/luci/gce/api/config/v1"
    27  	"go.chromium.org/luci/gce/appengine/model"
    28  	"go.chromium.org/luci/server/auth"
    29  	"go.chromium.org/luci/server/auth/authtest"
    30  
    31  	. "github.com/smartystreets/goconvey/convey"
    32  
    33  	. "go.chromium.org/luci/common/testing/assertions"
    34  )
    35  
    36  func TestConfig(t *testing.T) {
    37  	t.Parallel()
    38  
    39  	Convey("Config", t, func() {
    40  		srv := &Config{}
    41  		c := memory.Use(context.Background())
    42  		datastore.GetTestable(c).AutoIndex(true)
    43  		datastore.GetTestable(c).Consistent(true)
    44  
    45  		Convey("Delete", func() {
    46  			Convey("invalid", func() {
    47  				Convey("nil", func() {
    48  					cfg, err := srv.Delete(c, nil)
    49  					So(err, ShouldErrLike, "ID is required")
    50  					So(cfg, ShouldBeNil)
    51  				})
    52  
    53  				Convey("empty", func() {
    54  					cfg, err := srv.Delete(c, &config.DeleteRequest{})
    55  					So(err, ShouldErrLike, "ID is required")
    56  					So(cfg, ShouldBeNil)
    57  				})
    58  			})
    59  
    60  			Convey("valid", func() {
    61  				So(datastore.Put(c, &model.Config{
    62  					ID: "id",
    63  				}), ShouldBeNil)
    64  				cfg, err := srv.Delete(c, &config.DeleteRequest{
    65  					Id: "id",
    66  				})
    67  				So(err, ShouldBeNil)
    68  				So(cfg, ShouldResemble, &emptypb.Empty{})
    69  				err = datastore.Get(c, &model.Config{
    70  					ID: "id",
    71  				})
    72  				So(err, ShouldEqual, datastore.ErrNoSuchEntity)
    73  			})
    74  		})
    75  
    76  		Convey("Ensure", func() {
    77  			Convey("invalid", func() {
    78  				Convey("nil", func() {
    79  					cfg, err := srv.Ensure(c, nil)
    80  					So(err, ShouldErrLike, "ID is required")
    81  					So(cfg, ShouldBeNil)
    82  				})
    83  
    84  				Convey("empty", func() {
    85  					cfg, err := srv.Ensure(c, &config.EnsureRequest{})
    86  					So(err, ShouldErrLike, "ID is required")
    87  					So(cfg, ShouldBeNil)
    88  				})
    89  
    90  				Convey("ID", func() {
    91  					cfg, err := srv.Ensure(c, &config.EnsureRequest{
    92  						Config: &config.Config{},
    93  					})
    94  					So(err, ShouldErrLike, "ID is required")
    95  					So(cfg, ShouldBeNil)
    96  				})
    97  			})
    98  
    99  			Convey("valid", func() {
   100  				Convey("new", func() {
   101  					cfg, err := srv.Ensure(c, &config.EnsureRequest{
   102  						Id: "id",
   103  						Config: &config.Config{
   104  							Attributes: &config.VM{
   105  								Disk: []*config.Disk{
   106  									{},
   107  								},
   108  								MachineType: "type",
   109  								NetworkInterface: []*config.NetworkInterface{
   110  									{},
   111  								},
   112  								Project: "project",
   113  								Zone:    "zone",
   114  							},
   115  							Lifetime: &config.TimePeriod{
   116  								Time: &config.TimePeriod_Seconds{
   117  									Seconds: 3600,
   118  								},
   119  							},
   120  							Prefix: "prefix",
   121  						},
   122  					})
   123  					So(err, ShouldBeNil)
   124  					So(cfg, ShouldResembleProto, &config.Config{
   125  						Attributes: &config.VM{
   126  							Disk: []*config.Disk{
   127  								{},
   128  							},
   129  							MachineType: "type",
   130  							Project:     "project",
   131  							NetworkInterface: []*config.NetworkInterface{
   132  								{},
   133  							},
   134  							Zone: "zone",
   135  						},
   136  						Lifetime: &config.TimePeriod{
   137  							Time: &config.TimePeriod_Seconds{
   138  								Seconds: 3600,
   139  							},
   140  						},
   141  						Prefix: "prefix",
   142  					})
   143  				})
   144  
   145  				Convey("update doesn't erase currentAmount", func() {
   146  					So(datastore.Put(c, &model.Config{
   147  						ID: "id",
   148  						Config: &config.Config{
   149  							Amount: &config.Amount{
   150  								Max: 20,
   151  								Min: 10,
   152  							},
   153  							CurrentAmount: 15,
   154  							Owner: []string{
   155  								"owners",
   156  							},
   157  							Prefix: "prefix",
   158  						},
   159  					}), ShouldBeNil)
   160  
   161  					cfg, err := srv.Ensure(c, &config.EnsureRequest{
   162  						Id: "id",
   163  						Config: &config.Config{
   164  							Amount: &config.Amount{
   165  								Max: 100,
   166  								Min: 50,
   167  							},
   168  							Attributes: &config.VM{
   169  								Disk: []*config.Disk{
   170  									{},
   171  								},
   172  								MachineType: "type",
   173  								NetworkInterface: []*config.NetworkInterface{
   174  									{},
   175  								},
   176  								Project: "project",
   177  								Zone:    "zone",
   178  							},
   179  							Lifetime: &config.TimePeriod{
   180  								Time: &config.TimePeriod_Seconds{
   181  									Seconds: 3600,
   182  								},
   183  							},
   184  							Prefix: "prefix",
   185  						},
   186  					})
   187  					So(err, ShouldBeNil)
   188  					So(cfg, ShouldResembleProto, &config.Config{
   189  						Amount: &config.Amount{
   190  							Max: 100,
   191  							Min: 50,
   192  						},
   193  						CurrentAmount: 15, // Same as before.
   194  						Attributes: &config.VM{
   195  							Disk: []*config.Disk{
   196  								{},
   197  							},
   198  							MachineType: "type",
   199  							Project:     "project",
   200  							NetworkInterface: []*config.NetworkInterface{
   201  								{},
   202  							},
   203  							Zone: "zone",
   204  						},
   205  						Lifetime: &config.TimePeriod{
   206  							Time: &config.TimePeriod_Seconds{
   207  								Seconds: 3600,
   208  							},
   209  						},
   210  						Prefix: "prefix",
   211  					})
   212  				})
   213  			})
   214  		})
   215  
   216  		Convey("Get", func() {
   217  			Convey("invalid", func() {
   218  				Convey("nil", func() {
   219  					cfg, err := srv.Get(c, nil)
   220  					So(err, ShouldErrLike, "ID is required")
   221  					So(cfg, ShouldBeNil)
   222  				})
   223  
   224  				Convey("empty", func() {
   225  					cfg, err := srv.Get(c, &config.GetRequest{})
   226  					So(err, ShouldErrLike, "ID is required")
   227  					So(cfg, ShouldBeNil)
   228  				})
   229  
   230  				Convey("unauthorized owners", func() {
   231  					c = auth.WithState(c, &authtest.FakeState{
   232  						IdentityGroups: []string{"owners1"},
   233  					})
   234  					So(datastore.Put(c, &model.Config{
   235  						ID: "id",
   236  						Config: &config.Config{
   237  							Prefix: "prefix",
   238  							Owner: []string{
   239  								"owners2",
   240  							},
   241  						},
   242  					}), ShouldBeNil)
   243  					cfg, err := srv.Get(c, &config.GetRequest{
   244  						Id: "id",
   245  					})
   246  					So(err, ShouldErrLike, "no config found with ID \"id\" or unauthorized user")
   247  					So(cfg, ShouldBeNil)
   248  				})
   249  			})
   250  
   251  			Convey("valid", func() {
   252  				Convey("not found", func() {
   253  					cfg, err := srv.Get(c, &config.GetRequest{
   254  						Id: "id",
   255  					})
   256  					So(err, ShouldErrLike, "no config found with ID \"id\" or unauthorized user")
   257  					So(cfg, ShouldBeNil)
   258  				})
   259  
   260  				Convey("found", func() {
   261  					c = auth.WithState(c, &authtest.FakeState{
   262  						IdentityGroups: []string{"owners"},
   263  					})
   264  					So(datastore.Put(c, &model.Config{
   265  						ID: "id",
   266  						Config: &config.Config{
   267  							Prefix: "prefix",
   268  							Owner: []string{
   269  								"owners",
   270  							},
   271  						},
   272  					}), ShouldBeNil)
   273  					cfg, err := srv.Get(c, &config.GetRequest{
   274  						Id: "id",
   275  					})
   276  					So(err, ShouldBeNil)
   277  					So(cfg, ShouldResembleProto, &config.Config{
   278  						Prefix: "prefix",
   279  						Owner: []string{
   280  							"owners",
   281  						},
   282  					})
   283  				})
   284  			})
   285  		})
   286  
   287  		Convey("List", func() {
   288  			Convey("invalid", func() {
   289  				Convey("page token", func() {
   290  					req := &config.ListRequest{
   291  						PageToken: "token",
   292  					}
   293  					_, err := srv.List(c, req)
   294  					So(err, ShouldErrLike, "invalid page token")
   295  				})
   296  			})
   297  
   298  			Convey("valid", func() {
   299  				Convey("nil", func() {
   300  					Convey("none", func() {
   301  						rsp, err := srv.List(c, nil)
   302  						So(err, ShouldBeNil)
   303  						So(rsp.Configs, ShouldBeEmpty)
   304  					})
   305  
   306  					Convey("one", func() {
   307  						So(datastore.Put(c, &model.Config{
   308  							ID: "id",
   309  						}), ShouldBeNil)
   310  						rsp, err := srv.List(c, nil)
   311  						So(err, ShouldBeNil)
   312  						So(rsp.Configs, ShouldHaveLength, 1)
   313  					})
   314  				})
   315  
   316  				Convey("empty", func() {
   317  					Convey("none", func() {
   318  						req := &config.ListRequest{}
   319  						rsp, err := srv.List(c, req)
   320  						So(err, ShouldBeNil)
   321  						So(rsp.Configs, ShouldBeEmpty)
   322  					})
   323  
   324  					Convey("one", func() {
   325  						So(datastore.Put(c, &model.Config{
   326  							ID: "id",
   327  						}), ShouldBeNil)
   328  						req := &config.ListRequest{}
   329  						rsp, err := srv.List(c, req)
   330  						So(err, ShouldBeNil)
   331  						So(rsp.Configs, ShouldHaveLength, 1)
   332  					})
   333  				})
   334  
   335  				Convey("pages", func() {
   336  					So(datastore.Put(c, &model.Config{ID: "id1"}), ShouldBeNil)
   337  					So(datastore.Put(c, &model.Config{ID: "id2"}), ShouldBeNil)
   338  					So(datastore.Put(c, &model.Config{ID: "id3"}), ShouldBeNil)
   339  
   340  					Convey("default", func() {
   341  						req := &config.ListRequest{}
   342  						rsp, err := srv.List(c, req)
   343  						So(err, ShouldBeNil)
   344  						So(rsp.Configs, ShouldNotBeEmpty)
   345  					})
   346  
   347  					Convey("one", func() {
   348  						req := &config.ListRequest{
   349  							PageSize: 1,
   350  						}
   351  						rsp, err := srv.List(c, req)
   352  						So(err, ShouldBeNil)
   353  						So(rsp.Configs, ShouldHaveLength, 1)
   354  						So(rsp.NextPageToken, ShouldNotBeEmpty)
   355  
   356  						req.PageToken = rsp.NextPageToken
   357  						rsp, err = srv.List(c, req)
   358  						So(err, ShouldBeNil)
   359  						So(rsp.Configs, ShouldHaveLength, 1)
   360  
   361  						req.PageToken = rsp.NextPageToken
   362  						rsp, err = srv.List(c, req)
   363  						So(err, ShouldBeNil)
   364  						So(rsp.Configs, ShouldHaveLength, 1)
   365  						So(rsp.NextPageToken, ShouldBeEmpty)
   366  					})
   367  
   368  					Convey("two", func() {
   369  						req := &config.ListRequest{
   370  							PageSize: 2,
   371  						}
   372  						rsp, err := srv.List(c, req)
   373  						So(err, ShouldBeNil)
   374  						So(rsp.Configs, ShouldHaveLength, 2)
   375  						So(rsp.NextPageToken, ShouldNotBeEmpty)
   376  
   377  						req.PageToken = rsp.NextPageToken
   378  						rsp, err = srv.List(c, req)
   379  						So(err, ShouldBeNil)
   380  						So(rsp.Configs, ShouldHaveLength, 1)
   381  						So(rsp.NextPageToken, ShouldBeEmpty)
   382  					})
   383  
   384  					Convey("many", func() {
   385  						req := &config.ListRequest{
   386  							PageSize: 200,
   387  						}
   388  						rsp, err := srv.List(c, req)
   389  						So(err, ShouldBeNil)
   390  						So(rsp.Configs, ShouldHaveLength, 3)
   391  						So(rsp.NextPageToken, ShouldBeEmpty)
   392  					})
   393  				})
   394  			})
   395  		})
   396  
   397  		Convey("Update", func() {
   398  			c = auth.WithState(c, &authtest.FakeState{
   399  				IdentityGroups: []string{"owners"},
   400  			})
   401  			So(datastore.Put(c, &model.Config{
   402  				ID: "id",
   403  				Config: &config.Config{
   404  					Amount: &config.Amount{
   405  						Max: 3,
   406  						Min: 1,
   407  					},
   408  					CurrentAmount: 1,
   409  					Owner: []string{
   410  						"owners",
   411  					},
   412  					Prefix: "prefix",
   413  					Duts:   map[string]*emptypb.Empty{"dut1": {}},
   414  				},
   415  			}), ShouldBeNil)
   416  
   417  			Convey("invalid", func() {
   418  				Convey("nil", func() {
   419  					cfg, err := srv.Update(c, nil)
   420  					So(err, ShouldErrLike, "ID is required")
   421  					So(cfg, ShouldBeNil)
   422  					mdl := &model.Config{
   423  						ID: "id",
   424  					}
   425  					err = datastore.Get(c, mdl)
   426  					So(err, ShouldBeNil)
   427  					So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   428  				})
   429  
   430  				Convey("empty", func() {
   431  					cfg, err := srv.Update(c, &config.UpdateRequest{})
   432  					So(err, ShouldErrLike, "ID is required")
   433  					So(cfg, ShouldBeNil)
   434  					mdl := &model.Config{
   435  						ID: "id",
   436  					}
   437  					err = datastore.Get(c, mdl)
   438  					So(err, ShouldBeNil)
   439  					So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   440  				})
   441  
   442  				Convey("id", func() {
   443  					cfg, err := srv.Update(c, &config.UpdateRequest{
   444  						UpdateMask: &field_mask.FieldMask{
   445  							Paths: []string{
   446  								"config.current_amount",
   447  							},
   448  						},
   449  					})
   450  					So(err, ShouldErrLike, "ID is required")
   451  					So(cfg, ShouldBeNil)
   452  					mdl := &model.Config{
   453  						ID: "id",
   454  					}
   455  					err = datastore.Get(c, mdl)
   456  					So(err, ShouldBeNil)
   457  					So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   458  				})
   459  
   460  				Convey("update mask", func() {
   461  					Convey("missing", func() {
   462  						cfg, err := srv.Update(c, &config.UpdateRequest{
   463  							Id: "id",
   464  						})
   465  						So(err, ShouldErrLike, "update mask is required")
   466  						So(cfg, ShouldBeNil)
   467  						mdl := &model.Config{
   468  							ID: "id",
   469  						}
   470  						err = datastore.Get(c, mdl)
   471  						So(err, ShouldBeNil)
   472  						So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   473  					})
   474  
   475  					Convey("empty", func() {
   476  						cfg, err := srv.Update(c, &config.UpdateRequest{
   477  							Id: "id",
   478  							UpdateMask: &field_mask.FieldMask{
   479  								Paths: []string{},
   480  							},
   481  						})
   482  						So(err, ShouldErrLike, "update mask is required")
   483  						So(cfg, ShouldBeNil)
   484  						mdl := &model.Config{
   485  							ID: "id",
   486  						}
   487  						err = datastore.Get(c, mdl)
   488  						So(err, ShouldBeNil)
   489  						So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   490  					})
   491  
   492  					Convey("invalid mask", func() {
   493  						cfg, err := srv.Update(c, &config.UpdateRequest{
   494  							Id: "id",
   495  							UpdateMask: &field_mask.FieldMask{
   496  								Paths: []string{
   497  									"config.amount.default",
   498  								},
   499  							},
   500  						})
   501  						So(err, ShouldErrLike, "invalid or immutable")
   502  						So(cfg, ShouldBeNil)
   503  						mdl := &model.Config{
   504  							ID: "id",
   505  						}
   506  						err = datastore.Get(c, mdl)
   507  						So(err, ShouldBeNil)
   508  						So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   509  					})
   510  
   511  					Convey("immutable field", func() {
   512  						cfg, err := srv.Update(c, &config.UpdateRequest{
   513  							Id: "id",
   514  							UpdateMask: &field_mask.FieldMask{
   515  								Paths: []string{
   516  									"config.prefix",
   517  								},
   518  							},
   519  						})
   520  						So(err, ShouldErrLike, "invalid or immutable")
   521  						So(cfg, ShouldBeNil)
   522  						mdl := &model.Config{
   523  							ID: "id",
   524  						}
   525  						err = datastore.Get(c, mdl)
   526  						So(err, ShouldBeNil)
   527  						So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   528  					})
   529  				})
   530  			})
   531  
   532  			Convey("valid", func() {
   533  				Convey("unauthorized", func() {
   534  					c = auth.WithState(c, &authtest.FakeState{})
   535  					cfg, err := srv.Update(c, &config.UpdateRequest{
   536  						Id: "id",
   537  						Config: &config.Config{
   538  							CurrentAmount: 2,
   539  						},
   540  						UpdateMask: &field_mask.FieldMask{
   541  							Paths: []string{
   542  								"config.current_amount",
   543  							},
   544  						},
   545  					})
   546  					So(err, ShouldErrLike, "unauthorized user")
   547  					So(cfg, ShouldBeNil)
   548  					mdl := &model.Config{
   549  						ID: "id",
   550  					}
   551  					err = datastore.Get(c, mdl)
   552  					So(err, ShouldBeNil)
   553  					So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   554  				})
   555  
   556  				Convey("authorized", func() {
   557  					Convey("CurrentAmount", func() {
   558  						Convey("min", func() {
   559  							cfg, err := srv.Update(c, &config.UpdateRequest{
   560  								Id: "id",
   561  								UpdateMask: &field_mask.FieldMask{
   562  									Paths: []string{
   563  										"config.current_amount",
   564  									},
   565  								},
   566  							})
   567  							So(err, ShouldBeNil)
   568  							So(cfg.CurrentAmount, ShouldEqual, 1)
   569  							mdl := &model.Config{
   570  								ID: "id",
   571  							}
   572  							err = datastore.Get(c, mdl)
   573  							So(err, ShouldBeNil)
   574  							So(mdl.Config.CurrentAmount, ShouldEqual, 1)
   575  						})
   576  
   577  						Convey("max", func() {
   578  							cfg, err := srv.Update(c, &config.UpdateRequest{
   579  								Id: "id",
   580  								Config: &config.Config{
   581  									CurrentAmount: 4,
   582  								},
   583  								UpdateMask: &field_mask.FieldMask{
   584  									Paths: []string{
   585  										"config.current_amount",
   586  									},
   587  								},
   588  							})
   589  							So(err, ShouldBeNil)
   590  							So(cfg.CurrentAmount, ShouldEqual, 3)
   591  							mdl := &model.Config{
   592  								ID: "id",
   593  							}
   594  							err = datastore.Get(c, mdl)
   595  							So(err, ShouldBeNil)
   596  							So(mdl.Config.CurrentAmount, ShouldEqual, 3)
   597  						})
   598  
   599  						Convey("updates", func() {
   600  							cfg, err := srv.Update(c, &config.UpdateRequest{
   601  								Id: "id",
   602  								Config: &config.Config{
   603  									CurrentAmount: 2,
   604  								},
   605  								UpdateMask: &field_mask.FieldMask{
   606  									Paths: []string{
   607  										"config.current_amount",
   608  									},
   609  								},
   610  							})
   611  							So(err, ShouldBeNil)
   612  							So(cfg.CurrentAmount, ShouldEqual, 2)
   613  							mdl := &model.Config{
   614  								ID: "id",
   615  							}
   616  							err = datastore.Get(c, mdl)
   617  							So(err, ShouldBeNil)
   618  							So(mdl.Config.CurrentAmount, ShouldEqual, 2)
   619  						})
   620  					})
   621  					Convey("duts", func() {
   622  						Convey("updates", func() {
   623  							cfg, err := srv.Update(c, &config.UpdateRequest{
   624  								Id: "id",
   625  								Config: &config.Config{
   626  									Duts: map[string]*emptypb.Empty{
   627  										"hello": {},
   628  										"world": {},
   629  									},
   630  								},
   631  								UpdateMask: &field_mask.FieldMask{
   632  									Paths: []string{
   633  										"config.duts",
   634  									},
   635  								},
   636  							})
   637  							So(err, ShouldBeNil)
   638  							So(cfg.CurrentAmount, ShouldEqual, 2)
   639  							So(cfg.Duts, ShouldResembleProto, map[string]*emptypb.Empty{
   640  								"hello": {},
   641  								"world": {},
   642  							})
   643  							mdl := &model.Config{
   644  								ID: "id",
   645  							}
   646  							err = datastore.Get(c, mdl)
   647  							So(err, ShouldBeNil)
   648  							So(mdl.Config.CurrentAmount, ShouldEqual, 2)
   649  							So(mdl.Config.Duts, ShouldResembleProto, map[string]*emptypb.Empty{
   650  								"hello": {},
   651  								"world": {},
   652  							})
   653  						})
   654  					})
   655  				})
   656  			})
   657  		})
   658  	})
   659  	Convey("equalDuts", t, func() {
   660  		Convey("fail", func() {
   661  			Convey("different length", func() {
   662  				s1 := map[string]*emptypb.Empty{
   663  					"dut1": {},
   664  				}
   665  				s2 := map[string]*emptypb.Empty{
   666  					"dut1": {},
   667  					"dut2": {},
   668  				}
   669  				isEqual := dutsEqual(s1, s2)
   670  				So(isEqual, ShouldBeFalse)
   671  			})
   672  			Convey("different keys", func() {
   673  				s1 := map[string]*emptypb.Empty{
   674  					"dut1": {},
   675  				}
   676  				s2 := map[string]*emptypb.Empty{
   677  					"dut2": {},
   678  				}
   679  				isEqual := dutsEqual(s1, s2)
   680  				So(isEqual, ShouldBeFalse)
   681  			})
   682  		})
   683  		Convey("pass", func() {
   684  			Convey("same keys", func() {
   685  				s1 := map[string]*emptypb.Empty{
   686  					"dut1": {},
   687  					"dut2": {},
   688  				}
   689  				s2 := map[string]*emptypb.Empty{
   690  					"dut1": {},
   691  					"dut2": {},
   692  				}
   693  				isEqual := dutsEqual(s1, s2)
   694  				So(isEqual, ShouldBeTrue)
   695  			})
   696  			Convey("empty", func() {
   697  				s1 := map[string]*emptypb.Empty{}
   698  				s2 := map[string]*emptypb.Empty{}
   699  				isEqual := dutsEqual(s1, s2)
   700  				So(isEqual, ShouldBeTrue)
   701  			})
   702  		})
   703  	})
   704  }