github.com/zorawar87/trillian@v1.2.1/quota/etcd/quotaapi/quota_server_test.go (about)

     1  // Copyright 2017 Google Inc. All Rights Reserved.
     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 quotaapi
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"math/rand"
    21  	"net"
    22  	"os"
    23  	"sort"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  
    28  	"github.com/coreos/etcd/clientv3"
    29  	"github.com/golang/protobuf/proto"
    30  	"github.com/google/trillian/quota"
    31  	"github.com/google/trillian/quota/etcd/quotapb"
    32  	"github.com/google/trillian/quota/etcd/storage"
    33  	"github.com/google/trillian/quota/etcd/storagepb"
    34  	"github.com/google/trillian/server/interceptor"
    35  	"github.com/google/trillian/testonly/integration/etcd"
    36  	"github.com/kylelemons/godebug/pretty"
    37  	"google.golang.org/genproto/protobuf/field_mask"
    38  	"google.golang.org/grpc"
    39  	"google.golang.org/grpc/codes"
    40  	"google.golang.org/grpc/status"
    41  )
    42  
    43  var (
    44  	globalWrite = &quotapb.Config{
    45  		Name:          "quotas/global/write/config",
    46  		State:         quotapb.Config_ENABLED,
    47  		MaxTokens:     100,
    48  		CurrentTokens: 100, // Assume quota is full
    49  		ReplenishmentStrategy: &quotapb.Config_SequencingBased{
    50  			SequencingBased: &quotapb.SequencingBasedStrategy{},
    51  		},
    52  	}
    53  	globalRead = &quotapb.Config{
    54  		Name:          "quotas/global/read/config",
    55  		State:         quotapb.Config_ENABLED,
    56  		MaxTokens:     100,
    57  		CurrentTokens: 100, // Assume quota is full
    58  		ReplenishmentStrategy: &quotapb.Config_TimeBased{
    59  			TimeBased: &quotapb.TimeBasedStrategy{
    60  				TokensToReplenish:        10000,
    61  				ReplenishIntervalSeconds: 100,
    62  			},
    63  		},
    64  	}
    65  	tree1Read  = copyWithName(globalRead, "quotas/trees/1/read/config")
    66  	tree1Write = copyWithName(globalWrite, "quotas/trees/1/write/config")
    67  	tree2Read  = copyWithName(globalRead, "quotas/trees/2/read/config")
    68  	tree2Write = copyWithName(globalWrite, "quotas/trees/2/write/config")
    69  	userRead   = copyWithName(globalRead, "quotas/users/llama/read/config")
    70  	userWrite  = copyWithName(globalRead, "quotas/users/llama/write/config")
    71  
    72  	etcdClient  *clientv3.Client
    73  	quotaClient quotapb.QuotaClient
    74  )
    75  
    76  func copyWithName(c *quotapb.Config, name string) *quotapb.Config {
    77  	cp := *c
    78  	cp.Name = name
    79  	return &cp
    80  }
    81  
    82  func TestMain(m *testing.M) {
    83  	_, ec, stopEtcd, err := etcd.StartEtcd()
    84  	if err != nil {
    85  		panic(fmt.Sprintf("StartEtcd() returned err = %v", err))
    86  	}
    87  	// Don't defer stopEtcd(): the os.Exit() call below doesn't allow for defers.
    88  	etcdClient = ec
    89  
    90  	qc, stopServer, err := startServer(ec)
    91  	if err != nil {
    92  		stopEtcd()
    93  		panic(fmt.Sprintf("startServer() returned err = %v", err))
    94  	}
    95  	// Don't defer stopServer().
    96  	quotaClient = qc
    97  
    98  	exitCode := m.Run()
    99  	stopServer()
   100  	stopEtcd()
   101  	os.Exit(exitCode)
   102  }
   103  
   104  func TestServer_CreateConfig(t *testing.T) {
   105  	globalWrite2 := *globalWrite
   106  	globalWrite2.MaxTokens += 10
   107  	globalWrite2.CurrentTokens = globalWrite2.MaxTokens
   108  
   109  	overrideName := *globalWrite
   110  	overrideName.Name = "ignored"
   111  
   112  	invalidConfig := *globalWrite
   113  	invalidConfig.State = quotapb.Config_UNKNOWN_CONFIG_STATE
   114  
   115  	tests := []upsertTest{
   116  		{
   117  			desc:     "emptyName",
   118  			req:      &quotapb.CreateConfigRequest{Config: globalWrite},
   119  			wantCode: codes.InvalidArgument,
   120  		},
   121  		{
   122  			desc:     "emptyConfig",
   123  			req:      &quotapb.CreateConfigRequest{Name: globalWrite.Name},
   124  			wantCode: codes.InvalidArgument,
   125  		},
   126  		{
   127  			desc:    "success",
   128  			req:     &quotapb.CreateConfigRequest{Name: globalWrite.Name, Config: globalWrite},
   129  			wantCfg: globalWrite,
   130  		},
   131  		{
   132  			desc:     "alreadyExists",
   133  			baseCfg:  globalWrite,
   134  			req:      &quotapb.CreateConfigRequest{Name: globalWrite.Name, Config: &globalWrite2},
   135  			wantCode: codes.AlreadyExists,
   136  			wantCfg:  globalWrite,
   137  		},
   138  		{
   139  			desc: "overrideName",
   140  			req: &quotapb.CreateConfigRequest{
   141  				Name:   globalWrite.Name,
   142  				Config: &overrideName,
   143  			},
   144  			wantCfg: globalWrite,
   145  		},
   146  		{
   147  			desc: "invalidConfig",
   148  			req: &quotapb.CreateConfigRequest{
   149  				Name:   "quotas/global/read/config",
   150  				Config: &invalidConfig,
   151  			},
   152  			wantCode: codes.InvalidArgument,
   153  		},
   154  	}
   155  
   156  	ctx := context.Background()
   157  	rpc := func(ctx context.Context, req interface{}) (*quotapb.Config, error) {
   158  		return quotaClient.CreateConfig(ctx, req.(*quotapb.CreateConfigRequest))
   159  	}
   160  	for _, test := range tests {
   161  		if err := runUpsertTest(ctx, test, rpc, "CreateConfig"); err != nil {
   162  			t.Error(err)
   163  		}
   164  	}
   165  }
   166  
   167  func TestServer_UpdateConfig(t *testing.T) {
   168  	disabledGlobalWrite := *globalWrite
   169  	// Disabled quotas have "infinite" tokens
   170  	disabledGlobalWrite.CurrentTokens = int64(quota.MaxTokens)
   171  	disabledGlobalWrite.State = quotapb.Config_DISABLED
   172  
   173  	timeBasedGlobalWrite := *globalWrite
   174  	timeBasedGlobalWrite.MaxTokens += 100
   175  	timeBasedGlobalWrite.CurrentTokens = globalWrite.MaxTokens
   176  	timeBasedGlobalWrite.ReplenishmentStrategy = &quotapb.Config_TimeBased{
   177  		TimeBased: &quotapb.TimeBasedStrategy{
   178  			TokensToReplenish:        100,
   179  			ReplenishIntervalSeconds: 200,
   180  		},
   181  	}
   182  
   183  	timeBasedGlobalWrite2 := timeBasedGlobalWrite
   184  	timeBasedGlobalWrite2.CurrentTokens = timeBasedGlobalWrite.MaxTokens
   185  	timeBasedGlobalWrite2.GetTimeBased().TokensToReplenish += 50
   186  	timeBasedGlobalWrite2.GetTimeBased().ReplenishIntervalSeconds -= 20
   187  
   188  	tests := []upsertTest{
   189  		{
   190  			desc:    "disableQuota",
   191  			baseCfg: globalWrite,
   192  			req: &quotapb.UpdateConfigRequest{
   193  				Name:       globalWrite.Name,
   194  				Config:     &quotapb.Config{State: disabledGlobalWrite.State},
   195  				UpdateMask: &field_mask.FieldMask{Paths: []string{"state"}},
   196  			},
   197  			wantCfg: &disabledGlobalWrite,
   198  		},
   199  		{
   200  			desc:    "timeBasedGlobalWrite",
   201  			baseCfg: globalWrite,
   202  			req: &quotapb.UpdateConfigRequest{
   203  				Name: globalWrite.Name,
   204  				Config: &quotapb.Config{
   205  					MaxTokens:             timeBasedGlobalWrite.MaxTokens,
   206  					ReplenishmentStrategy: timeBasedGlobalWrite.ReplenishmentStrategy,
   207  				},
   208  				UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens", "time_based"}},
   209  			},
   210  			wantCfg: &timeBasedGlobalWrite,
   211  		},
   212  		{
   213  			desc:    "timeBasedGlobalWrite2",
   214  			baseCfg: &timeBasedGlobalWrite,
   215  			req: &quotapb.UpdateConfigRequest{
   216  				Name: timeBasedGlobalWrite.Name,
   217  				Config: &quotapb.Config{
   218  					ReplenishmentStrategy: timeBasedGlobalWrite2.ReplenishmentStrategy,
   219  				},
   220  				UpdateMask: &field_mask.FieldMask{Paths: []string{"time_based"}},
   221  			},
   222  			wantCfg: &timeBasedGlobalWrite2,
   223  		},
   224  		{
   225  			desc: "unknownConfig",
   226  			req: &quotapb.UpdateConfigRequest{
   227  				Name:       globalWrite.Name,
   228  				Config:     &quotapb.Config{MaxTokens: 200},
   229  				UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens"}},
   230  			},
   231  			wantCode: codes.NotFound,
   232  		},
   233  		{
   234  			desc:    "badConfig",
   235  			baseCfg: globalWrite,
   236  			req: &quotapb.UpdateConfigRequest{
   237  				Name:       globalWrite.Name,
   238  				Config:     &quotapb.Config{}, // State == UNKNOWN
   239  				UpdateMask: &field_mask.FieldMask{Paths: []string{"state"}},
   240  			},
   241  			wantCode: codes.InvalidArgument,
   242  			wantCfg:  globalWrite,
   243  		},
   244  		{
   245  			desc:    "badPath",
   246  			baseCfg: globalWrite,
   247  			req: &quotapb.UpdateConfigRequest{
   248  				Name:       globalWrite.Name,
   249  				Config:     &quotapb.Config{},
   250  				UpdateMask: &field_mask.FieldMask{Paths: []string{"NOT_A_FIELD"}},
   251  			},
   252  			wantCode: codes.InvalidArgument,
   253  			wantCfg:  globalWrite,
   254  		},
   255  		{
   256  			desc: "emptyName",
   257  			req: &quotapb.UpdateConfigRequest{
   258  				Config:     &quotapb.Config{MaxTokens: 100},
   259  				UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens"}},
   260  			},
   261  			wantCode: codes.InvalidArgument,
   262  		},
   263  		{
   264  			desc:    "emptyConfig",
   265  			baseCfg: globalWrite,
   266  			req: &quotapb.UpdateConfigRequest{
   267  				Name:       globalWrite.Name,
   268  				UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens"}},
   269  			},
   270  			wantCode: codes.InvalidArgument,
   271  			wantCfg:  globalWrite,
   272  		},
   273  		{
   274  			desc:    "emptyMask",
   275  			baseCfg: globalWrite,
   276  			req: &quotapb.UpdateConfigRequest{
   277  				Name:   globalWrite.Name,
   278  				Config: &quotapb.Config{MaxTokens: 100},
   279  			},
   280  			wantCode: codes.InvalidArgument,
   281  			wantCfg:  globalWrite,
   282  		},
   283  		{
   284  			desc:    "emptyConfigWithReset",
   285  			baseCfg: globalWrite,
   286  			req: &quotapb.UpdateConfigRequest{
   287  				Name:       globalWrite.Name,
   288  				UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens"}},
   289  				ResetQuota: true,
   290  			},
   291  			wantCode: codes.InvalidArgument,
   292  			wantCfg:  globalWrite,
   293  		},
   294  		{
   295  			desc:    "emptyMaskWithReset",
   296  			baseCfg: globalWrite,
   297  			req: &quotapb.UpdateConfigRequest{
   298  				Name:       globalWrite.Name,
   299  				Config:     &quotapb.Config{MaxTokens: 100},
   300  				ResetQuota: true,
   301  			},
   302  			wantCode: codes.InvalidArgument,
   303  			wantCfg:  globalWrite,
   304  		},
   305  	}
   306  
   307  	ctx := context.Background()
   308  	rpc := func(ctx context.Context, req interface{}) (*quotapb.Config, error) {
   309  		return quotaClient.UpdateConfig(ctx, req.(*quotapb.UpdateConfigRequest))
   310  	}
   311  	for _, test := range tests {
   312  		if err := runUpsertTest(ctx, test, rpc, "UpdateConfig"); err != nil {
   313  			t.Error(err)
   314  		}
   315  	}
   316  }
   317  
   318  func TestServer_UpdateConfig_ResetQuota(t *testing.T) {
   319  	ctx := context.Background()
   320  	if err := reset(ctx); err != nil {
   321  		t.Fatalf("reset() returned err = %v", err)
   322  	}
   323  	if _, err := quotaClient.CreateConfig(ctx, &quotapb.CreateConfigRequest{
   324  		Name:   globalWrite.Name,
   325  		Config: globalWrite,
   326  	}); err != nil {
   327  		t.Fatalf("CreateConfig() returned err = %v", err)
   328  	}
   329  	qs := storage.QuotaStorage{Client: etcdClient}
   330  	if err := qs.Get(ctx, []string{globalWrite.Name}, globalWrite.MaxTokens-1); err != nil {
   331  		t.Fatalf("Get() returned err = %v", err)
   332  	}
   333  
   334  	cfg := *globalWrite
   335  	cfg.MaxTokens += 10
   336  
   337  	tests := []struct {
   338  		desc       string
   339  		req        *quotapb.UpdateConfigRequest
   340  		wantTokens int64
   341  	}{
   342  		{
   343  			desc: "resetFalse",
   344  			req: &quotapb.UpdateConfigRequest{
   345  				Name:       globalWrite.Name,
   346  				Config:     &cfg,
   347  				UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens"}},
   348  			},
   349  			wantTokens: 1,
   350  		},
   351  		{
   352  			desc:       "resetTrue",
   353  			req:        &quotapb.UpdateConfigRequest{Name: globalWrite.Name, ResetQuota: true},
   354  			wantTokens: cfg.MaxTokens,
   355  		},
   356  	}
   357  	for _, test := range tests {
   358  		if _, err := quotaClient.UpdateConfig(ctx, test.req); err != nil {
   359  			t.Errorf("%v: UpdateConfig() returned err = %v", test.desc, err)
   360  			continue
   361  		}
   362  
   363  		tokens, err := qs.Peek(ctx, []string{test.req.Name})
   364  		if err != nil {
   365  			t.Fatalf("%v: Peek() returned err = %v", test.desc, err)
   366  		}
   367  		if got := tokens[test.req.Name]; got != test.wantTokens {
   368  			t.Errorf("%v: %q has %v tokens, want = %v", test.desc, test.req.Name, got, test.wantTokens)
   369  		}
   370  	}
   371  }
   372  
   373  func TestServer_UpdateConfig_Race(t *testing.T) {
   374  	if testing.Short() {
   375  		t.Skip("Skipping race test due to short mode")
   376  		return
   377  	}
   378  
   379  	ctx := context.Background()
   380  	if err := reset(ctx); err != nil {
   381  		t.Fatalf("reset() returned err = %v", err)
   382  	}
   383  
   384  	// Quotas we'll use for the test, a few of each type
   385  	configs := []*quotapb.Config{globalRead, globalWrite, tree1Read, tree1Write, userRead, userWrite}
   386  
   387  	// Create the configs we'll use for the test concurrently.
   388  	// If we get any errors a msg is sent to createErrs; that's a sign to stop
   389  	// the test *after* all spawned goroutines have exited.
   390  	createErrs := make(chan bool, len(configs))
   391  	wg := sync.WaitGroup{}
   392  	for _, cfg := range configs {
   393  		cfg := cfg
   394  		wg.Add(1)
   395  		go func() {
   396  			defer wg.Done()
   397  			if _, err := quotaClient.CreateConfig(ctx, &quotapb.CreateConfigRequest{
   398  				Name:   cfg.Name,
   399  				Config: cfg,
   400  			}); err != nil {
   401  				t.Errorf("%v: CreateConfig() returned err = %v", cfg.Name, err)
   402  				createErrs <- true
   403  			}
   404  		}()
   405  	}
   406  	wg.Wait()
   407  	select {
   408  	case <-createErrs:
   409  		return
   410  	default:
   411  	}
   412  
   413  	const routinesPerConfig = 4
   414  	const updatesPerRoutine = 100
   415  	for _, cfg := range configs {
   416  		for num := 0; num < routinesPerConfig; num++ {
   417  			wg.Add(1)
   418  			go func(num int, want quotapb.Config) {
   419  				defer wg.Done()
   420  				baseTokens := 1 + rand.Intn(routinesPerConfig*100)
   421  				reset := num%2 == 0
   422  				for i := 0; i < updatesPerRoutine; i++ {
   423  					tokens := int64(baseTokens + i)
   424  					got, err := quotaClient.UpdateConfig(ctx, &quotapb.UpdateConfigRequest{
   425  						Name:       want.Name,
   426  						Config:     &quotapb.Config{MaxTokens: tokens},
   427  						UpdateMask: &field_mask.FieldMask{Paths: []string{"max_tokens"}},
   428  						ResetQuota: reset,
   429  					})
   430  					if err != nil {
   431  						t.Errorf("%v: UpdateConfig() returned err = %v", want.Name, err)
   432  						continue
   433  					}
   434  
   435  					want.CurrentTokens = got.CurrentTokens // Not important for this test
   436  					want.MaxTokens = tokens
   437  					if !proto.Equal(got, &want) {
   438  						diff := pretty.Compare(got, &want)
   439  						t.Errorf("%v: post-UpdateConfig() diff (-got +want):\n%v", want.Name, diff)
   440  					}
   441  				}
   442  			}(num, *cfg)
   443  		}
   444  	}
   445  	wg.Wait()
   446  }
   447  
   448  // upsertTest represents either a CreateConfig or UpdateConfig test.
   449  type upsertTest struct {
   450  	desc string
   451  	// baseCfg is created before calling the RPC, if non-nil.
   452  	baseCfg  *quotapb.Config
   453  	req      upsertRequest
   454  	wantCode codes.Code
   455  	wantCfg  *quotapb.Config
   456  }
   457  
   458  type upsertRequest interface {
   459  	GetName() string
   460  }
   461  
   462  type upsertRPC func(ctx context.Context, req interface{}) (*quotapb.Config, error)
   463  
   464  // runUpsertTest runs either CreateConfig or UpdateConfig tests, depending on the supplied rpc.
   465  // Storage is reset() before the test is executed.
   466  func runUpsertTest(ctx context.Context, test upsertTest, rpc upsertRPC, rpcName string) error {
   467  	if err := reset(ctx); err != nil {
   468  		return fmt.Errorf("%v: reset() returned err = %v", test.desc, err)
   469  	}
   470  	if test.baseCfg != nil {
   471  		if _, err := quotaClient.CreateConfig(ctx, &quotapb.CreateConfigRequest{
   472  			Name:   test.baseCfg.Name,
   473  			Config: test.baseCfg,
   474  		}); err != nil {
   475  			return fmt.Errorf("%v: CreateConfig() returned err = %v", test.desc, err)
   476  		}
   477  	}
   478  
   479  	cfg, err := rpc(ctx, test.req)
   480  	switch s, ok := status.FromError(err); {
   481  	case !ok || s.Code() != test.wantCode:
   482  		return fmt.Errorf("%v: %v() returned err = %v, wantCode = %s", test.desc, rpcName, err, test.wantCode)
   483  	case test.wantCode == codes.OK && !proto.Equal(cfg, test.wantCfg):
   484  		return fmt.Errorf("%v: post-%v() diff:\n%v", test.desc, rpcName, pretty.Compare(cfg, test.wantCfg))
   485  	case test.wantCfg == nil:
   486  		return nil
   487  	}
   488  
   489  	switch stored, err := quotaClient.GetConfig(ctx, &quotapb.GetConfigRequest{Name: test.req.GetName()}); {
   490  	case err != nil:
   491  		return fmt.Errorf("%v: GetConfig() returned err = %v", test.desc, err)
   492  	case !proto.Equal(stored, test.wantCfg):
   493  		return fmt.Errorf("%v: post-GetConfig() diff:\n%v", test.desc, pretty.Compare(stored, test.wantCfg))
   494  	}
   495  
   496  	return nil
   497  }
   498  
   499  func TestServer_DeleteConfig(t *testing.T) {
   500  	tests := []struct {
   501  		desc                 string
   502  		createCfgs, wantCfgs []*quotapb.Config
   503  		req                  *quotapb.DeleteConfigRequest
   504  	}{
   505  		{
   506  			desc: "success",
   507  			createCfgs: []*quotapb.Config{
   508  				globalRead, globalWrite,
   509  				tree1Read, tree1Write,
   510  				tree2Read, tree2Write,
   511  				userRead, userWrite,
   512  			},
   513  			wantCfgs: []*quotapb.Config{
   514  				globalRead, globalWrite,
   515  				tree1Write,
   516  				tree2Read, tree2Write,
   517  				userRead, userWrite,
   518  			},
   519  			req: &quotapb.DeleteConfigRequest{Name: tree1Read.Name},
   520  		},
   521  		{
   522  			desc:       "lastConfig",
   523  			createCfgs: []*quotapb.Config{tree1Read},
   524  			req:        &quotapb.DeleteConfigRequest{Name: tree1Read.Name},
   525  		},
   526  		{
   527  			desc:       "global",
   528  			createCfgs: []*quotapb.Config{globalRead, globalWrite},
   529  			wantCfgs:   []*quotapb.Config{globalRead},
   530  			req:        &quotapb.DeleteConfigRequest{Name: globalWrite.Name},
   531  		},
   532  		{
   533  			desc:       "user",
   534  			createCfgs: []*quotapb.Config{userRead, userWrite},
   535  			wantCfgs:   []*quotapb.Config{userWrite},
   536  			req:        &quotapb.DeleteConfigRequest{Name: userRead.Name},
   537  		},
   538  	}
   539  
   540  	ctx := context.Background()
   541  	for _, test := range tests {
   542  		if err := reset(ctx); err != nil {
   543  			t.Errorf("%v: reset() returned err = %v", test.desc, err)
   544  			continue
   545  		}
   546  
   547  		for _, cfg := range test.createCfgs {
   548  			if _, err := quotaClient.CreateConfig(ctx, &quotapb.CreateConfigRequest{
   549  				Name:   cfg.Name,
   550  				Config: cfg,
   551  			}); err != nil {
   552  				t.Fatalf("%v: CreateConfig(%q) returned err = %v", test.desc, cfg.Name, err)
   553  			}
   554  		}
   555  
   556  		if _, err := quotaClient.DeleteConfig(ctx, test.req); err != nil {
   557  			t.Errorf("%v: DeleteConfig(%q) returned err = %v", test.desc, test.req.Name, err)
   558  			continue
   559  		}
   560  
   561  		resp, err := quotaClient.ListConfigs(ctx, &quotapb.ListConfigsRequest{
   562  			View: quotapb.ListConfigsRequest_FULL,
   563  		})
   564  		if err != nil {
   565  			t.Errorf("%v: ListConfigs() returned err = %v", test.desc, err)
   566  			continue
   567  		}
   568  		if err := sortAndCompare(resp.Configs, test.wantCfgs); err != nil {
   569  			t.Errorf("%v: post-DeleteConfig() %v", test.desc, err)
   570  		}
   571  	}
   572  }
   573  
   574  func TestServer_DeleteConfigErrors(t *testing.T) {
   575  	tests := []struct {
   576  		desc     string
   577  		req      *quotapb.DeleteConfigRequest
   578  		wantCode codes.Code
   579  	}{
   580  		{
   581  			desc:     "emptyName",
   582  			req:      &quotapb.DeleteConfigRequest{},
   583  			wantCode: codes.InvalidArgument,
   584  		},
   585  		{
   586  			desc:     "badName",
   587  			req:      &quotapb.DeleteConfigRequest{Name: "bad/quota/name"},
   588  			wantCode: codes.InvalidArgument,
   589  		},
   590  		{
   591  			desc:     "unknown",
   592  			req:      &quotapb.DeleteConfigRequest{Name: globalWrite.Name},
   593  			wantCode: codes.NotFound,
   594  		},
   595  	}
   596  
   597  	ctx := context.Background()
   598  	for _, test := range tests {
   599  		_, err := quotaClient.DeleteConfig(ctx, test.req)
   600  		if s, ok := status.FromError(err); !ok || s.Code() != test.wantCode {
   601  			t.Errorf("%v: DeleteConfig() returned err = %v, wantCode = %s", test.desc, err, test.wantCode)
   602  		}
   603  	}
   604  }
   605  
   606  func TestServer_GetConfigErrors(t *testing.T) {
   607  	ctx := context.Background()
   608  	if err := reset(ctx); err != nil {
   609  		t.Fatalf("reset() returned err = %v", err)
   610  	}
   611  
   612  	tests := []struct {
   613  		desc     string
   614  		req      *quotapb.GetConfigRequest
   615  		wantCode codes.Code
   616  	}{
   617  		{
   618  			desc:     "emptyName",
   619  			req:      &quotapb.GetConfigRequest{},
   620  			wantCode: codes.InvalidArgument,
   621  		},
   622  		{
   623  			desc:     "badName",
   624  			req:      &quotapb.GetConfigRequest{Name: "not/a/config/name"},
   625  			wantCode: codes.InvalidArgument,
   626  		},
   627  		{
   628  			desc:     "notFound",
   629  			req:      &quotapb.GetConfigRequest{Name: "quotas/global/write/config"},
   630  			wantCode: codes.NotFound,
   631  		},
   632  	}
   633  	for _, test := range tests {
   634  		// GetConfig's success return is tested as a consequence of other test cases.
   635  		_, err := quotaClient.GetConfig(ctx, test.req)
   636  		if s, ok := status.FromError(err); !ok || s.Code() != test.wantCode {
   637  			t.Errorf("%v: GetConfig() returned err = %v, wantCode = %s", test.desc, err, test.wantCode)
   638  		}
   639  	}
   640  }
   641  
   642  func TestServer_ListConfigs(t *testing.T) {
   643  	ctx := context.Background()
   644  	if err := reset(ctx); err != nil {
   645  		t.Fatalf("reset() returned err = %v", err)
   646  	}
   647  
   648  	// Test listing with an empty config first, so we can work with a fixed list of configs from
   649  	// here on
   650  	resp, err := quotaClient.ListConfigs(ctx, &quotapb.ListConfigsRequest{})
   651  	switch {
   652  	case err != nil:
   653  		t.Errorf("empty: ListConfigs() returned err = %v", err)
   654  	case len(resp.Configs) != 0:
   655  		t.Errorf("empty: ListConfigs() returned >0 results: %v", err)
   656  	}
   657  
   658  	configs := []*quotapb.Config{
   659  		globalRead, globalWrite,
   660  		tree1Read, tree1Write,
   661  		tree2Read, tree2Write,
   662  		userRead, userWrite,
   663  	}
   664  	for _, cfg := range configs {
   665  		if _, err := quotaClient.CreateConfig(ctx, &quotapb.CreateConfigRequest{
   666  			Name:   cfg.Name,
   667  			Config: cfg,
   668  		}); err != nil {
   669  			t.Fatalf("%q: CreateConfig() returned err = %v", cfg.Name, err)
   670  		}
   671  	}
   672  
   673  	basicGlobalRead := &quotapb.Config{Name: globalRead.Name}
   674  	basicGlobalWrite := &quotapb.Config{Name: globalWrite.Name}
   675  	basicTree1Read := &quotapb.Config{Name: tree1Read.Name}
   676  	basicTree1Write := &quotapb.Config{Name: tree1Write.Name}
   677  	basicTree2Read := &quotapb.Config{Name: tree2Read.Name}
   678  	basicTree2Write := &quotapb.Config{Name: tree2Write.Name}
   679  	basicUserRead := &quotapb.Config{Name: userRead.Name}
   680  	basicUserWrite := &quotapb.Config{Name: userWrite.Name}
   681  
   682  	tests := []struct {
   683  		desc     string
   684  		req      *quotapb.ListConfigsRequest
   685  		wantCfgs []*quotapb.Config
   686  	}{
   687  		{
   688  			desc: "allBasicView",
   689  			req:  &quotapb.ListConfigsRequest{},
   690  			wantCfgs: []*quotapb.Config{
   691  				basicGlobalRead, basicGlobalWrite,
   692  				basicTree1Read, basicTree1Write,
   693  				basicTree2Read, basicTree2Write,
   694  				basicUserRead, basicUserWrite,
   695  			},
   696  		},
   697  		{
   698  			desc:     "allFullView",
   699  			req:      &quotapb.ListConfigsRequest{View: quotapb.ListConfigsRequest_FULL},
   700  			wantCfgs: configs,
   701  		},
   702  		{
   703  			desc:     "allTrees",
   704  			req:      &quotapb.ListConfigsRequest{Names: []string{"quotas/trees/-/-/config"}},
   705  			wantCfgs: []*quotapb.Config{basicTree1Read, basicTree1Write, basicTree2Read, basicTree2Write},
   706  		},
   707  		{
   708  			desc:     "allUsers",
   709  			req:      &quotapb.ListConfigsRequest{Names: []string{"quotas/users/-/-/config"}},
   710  			wantCfgs: []*quotapb.Config{basicUserRead, basicUserWrite},
   711  		},
   712  		{
   713  			desc: "unknowns",
   714  			req: &quotapb.ListConfigsRequest{
   715  				Names: []string{
   716  					"quotas/trees/99997/read/config",
   717  					"quotas/trees/99998/write/config",
   718  					"quotas/trees/99999/-/config",
   719  					"quotas/users/unknown/-/config",
   720  				},
   721  			},
   722  		},
   723  	}
   724  	for _, test := range tests {
   725  		resp, err := quotaClient.ListConfigs(ctx, test.req)
   726  		if err != nil {
   727  			t.Errorf("%v: ListConfigs() returned err = %v", test.desc, err)
   728  			continue
   729  		}
   730  		if err := sortAndCompare(resp.Configs, test.wantCfgs); err != nil {
   731  			t.Errorf("%v: post-ListConfigs() %v", test.desc, err)
   732  		}
   733  	}
   734  }
   735  
   736  func sortAndCompare(got, want []*quotapb.Config) error {
   737  	sort.Slice(got, func(i, j int) bool { return strings.Compare(got[i].Name, got[j].Name) == -1 })
   738  	sort.Slice(want, func(i, j int) bool { return strings.Compare(want[i].Name, want[j].Name) == -1 })
   739  	if len(got) != len(want) {
   740  		return fmt.Errorf("got %v configs, want %v", len(got), len(want))
   741  	}
   742  	for i, cfg := range want {
   743  		if !proto.Equal(got[i], cfg) {
   744  			return fmt.Errorf("diff (-got +want):\n%v", pretty.Compare(got, want))
   745  		}
   746  	}
   747  	return nil
   748  }
   749  
   750  func TestServer_ListConfigsErrors(t *testing.T) {
   751  	tests := []struct {
   752  		desc     string
   753  		req      *quotapb.ListConfigsRequest
   754  		wantCode codes.Code
   755  	}{
   756  		{
   757  			desc:     "badNameFilter",
   758  			req:      &quotapb.ListConfigsRequest{Names: []string{"bad/quota/name"}},
   759  			wantCode: codes.InvalidArgument,
   760  		},
   761  	}
   762  
   763  	ctx := context.Background()
   764  	for _, test := range tests {
   765  		_, err := quotaClient.ListConfigs(ctx, test.req)
   766  		if s, ok := status.FromError(err); !ok || s.Code() != test.wantCode {
   767  			t.Errorf("%v: ListConfigs() returned err = %v, wantCode = %s", test.desc, err, test.wantCode)
   768  		}
   769  	}
   770  }
   771  
   772  func reset(ctx context.Context) error {
   773  	qs := storage.QuotaStorage{Client: etcdClient}
   774  	_, err := qs.UpdateConfigs(ctx, true /* reset */, func(c *storagepb.Configs) { *c = storagepb.Configs{} })
   775  	return err
   776  }
   777  
   778  func startServer(etcdClient *clientv3.Client) (quotapb.QuotaClient, func(), error) {
   779  	var lis net.Listener
   780  	var s *grpc.Server
   781  	var conn *grpc.ClientConn
   782  
   783  	cleanup := func() {
   784  		if conn != nil {
   785  			conn.Close()
   786  		}
   787  		if s != nil {
   788  			s.GracefulStop()
   789  		}
   790  		if lis != nil {
   791  			lis.Close()
   792  		}
   793  	}
   794  
   795  	var err error
   796  	lis, err = net.Listen("tcp", "localhost:0")
   797  	if err != nil {
   798  		cleanup()
   799  		return nil, nil, err
   800  	}
   801  
   802  	s = grpc.NewServer(grpc.UnaryInterceptor(interceptor.ErrorWrapper))
   803  	quotapb.RegisterQuotaServer(s, NewServer(etcdClient))
   804  	go s.Serve(lis)
   805  
   806  	conn, err = grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
   807  	if err != nil {
   808  		cleanup()
   809  		return nil, nil, err
   810  	}
   811  
   812  	quotaClient := quotapb.NewQuotaClient(conn)
   813  	return quotaClient, cleanup, nil
   814  }