agones.dev/agones@v1.54.0/pkg/apis/allocation/v1/gameserverallocation_test.go (about)

     1  // Copyright 2019 Google LLC 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 v1
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"testing"
    21  
    22  	"agones.dev/agones/pkg/apis"
    23  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    24  	"agones.dev/agones/pkg/util/runtime"
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  )
    30  
    31  func TestGameServerAllocationApplyDefaults(t *testing.T) {
    32  	t.Parallel()
    33  
    34  	gsa := &GameServerAllocation{}
    35  	gsa.ApplyDefaults()
    36  
    37  	assert.Equal(t, apis.Packed, gsa.Spec.Scheduling)
    38  
    39  	priorities := []agonesv1.Priority{
    40  		{Type: agonesv1.GameServerPriorityList},
    41  		{Type: agonesv1.GameServerPriorityCounter},
    42  	}
    43  	expectedPrioritiesWithDefault := []agonesv1.Priority{
    44  		{Type: agonesv1.GameServerPriorityList, Order: agonesv1.GameServerPriorityAscending},
    45  		{Type: agonesv1.GameServerPriorityCounter, Order: agonesv1.GameServerPriorityAscending},
    46  	}
    47  
    48  	gsa = &GameServerAllocation{Spec: GameServerAllocationSpec{Scheduling: apis.Distributed, Priorities: priorities}}
    49  	gsa.ApplyDefaults()
    50  	assert.Equal(t, apis.Distributed, gsa.Spec.Scheduling)
    51  	assert.Equal(t, expectedPrioritiesWithDefault, gsa.Spec.Priorities)
    52  
    53  	runtime.FeatureTestMutex.Lock()
    54  	defer runtime.FeatureTestMutex.Unlock()
    55  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s=true", runtime.FeaturePlayerAllocationFilter, runtime.FeatureCountsAndLists)))
    56  
    57  	gsa = &GameServerAllocation{}
    58  	gsa.ApplyDefaults()
    59  
    60  	assert.Equal(t, agonesv1.GameServerStateReady, *gsa.Spec.Required.GameServerState)
    61  	assert.Equal(t, int64(0), gsa.Spec.Required.Players.MaxAvailable)
    62  	assert.Equal(t, int64(0), gsa.Spec.Required.Players.MinAvailable)
    63  	assert.Equal(t, []agonesv1.Priority(nil), gsa.Spec.Priorities)
    64  	assert.Nil(t, gsa.Spec.Priorities)
    65  }
    66  
    67  // nolint // Current lint duplicate threshold will consider this function is a duplication of the function TestGameServerAllocationSpecSelectors
    68  func TestGameServerAllocationSpecPreferredSelectors(t *testing.T) {
    69  	t.Parallel()
    70  
    71  	gsas := &GameServerAllocationSpec{
    72  		Preferred: []GameServerSelector{
    73  			{LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"check": "blue"}}},
    74  			{LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"check": "red"}}},
    75  		},
    76  	}
    77  
    78  	require.Len(t, gsas.Preferred, 2)
    79  
    80  	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}
    81  
    82  	for _, s := range gsas.Preferred {
    83  		assert.False(t, s.Matches(gs))
    84  	}
    85  
    86  	gs.ObjectMeta.Labels["check"] = "blue"
    87  	assert.True(t, gsas.Preferred[0].Matches(gs))
    88  	assert.False(t, gsas.Preferred[1].Matches(gs))
    89  
    90  	gs.ObjectMeta.Labels["check"] = "red"
    91  	assert.False(t, gsas.Preferred[0].Matches(gs))
    92  	assert.True(t, gsas.Preferred[1].Matches(gs))
    93  }
    94  
    95  // nolint // Current lint duplicate threshold will consider this function is a duplication of the function TestGameServerAllocationSpecPreferredSelectors
    96  func TestGameServerAllocationSpecSelectors(t *testing.T) {
    97  	t.Parallel()
    98  
    99  	gsas := &GameServerAllocationSpec{
   100  		Selectors: []GameServerSelector{
   101  			{LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"check": "blue"}}},
   102  			{LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"check": "red"}}},
   103  		},
   104  	}
   105  
   106  	require.Len(t, gsas.Selectors, 2)
   107  
   108  	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}
   109  
   110  	for _, s := range gsas.Selectors {
   111  		assert.False(t, s.Matches(gs))
   112  	}
   113  
   114  	gs.ObjectMeta.Labels["check"] = "blue"
   115  	assert.True(t, gsas.Selectors[0].Matches(gs))
   116  	assert.False(t, gsas.Selectors[1].Matches(gs))
   117  
   118  	gs.ObjectMeta.Labels["check"] = "red"
   119  	assert.False(t, gsas.Selectors[0].Matches(gs))
   120  	assert.True(t, gsas.Selectors[1].Matches(gs))
   121  }
   122  
   123  func TestGameServerSelectorApplyDefaults(t *testing.T) {
   124  	t.Parallel()
   125  	runtime.FeatureTestMutex.Lock()
   126  	defer runtime.FeatureTestMutex.Unlock()
   127  
   128  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s=true",
   129  		runtime.FeaturePlayerAllocationFilter,
   130  		runtime.FeatureCountsAndLists)))
   131  
   132  	s := &GameServerSelector{}
   133  
   134  	// no defaults
   135  	s.ApplyDefaults()
   136  	assert.Equal(t, agonesv1.GameServerStateReady, *s.GameServerState)
   137  	assert.Equal(t, int64(0), s.Players.MinAvailable)
   138  	assert.Equal(t, int64(0), s.Players.MaxAvailable)
   139  	assert.NotNil(t, s.Counters)
   140  	assert.NotNil(t, s.Lists)
   141  
   142  	// Test apply defaults is idempotent -- calling ApplyDefaults more than one time does not change the original result.
   143  	s.ApplyDefaults()
   144  	assert.Equal(t, agonesv1.GameServerStateReady, *s.GameServerState)
   145  	assert.Equal(t, int64(0), s.Players.MinAvailable)
   146  	assert.Equal(t, int64(0), s.Players.MaxAvailable)
   147  	assert.NotNil(t, s.Counters)
   148  	assert.NotNil(t, s.Lists)
   149  
   150  	state := agonesv1.GameServerStateAllocated
   151  	// set values
   152  	s = &GameServerSelector{
   153  		GameServerState: &state,
   154  		Players:         &PlayerSelector{MinAvailable: 10, MaxAvailable: 20},
   155  		Counters:        map[string]CounterSelector{"foo": {MinAvailable: 1, MaxAvailable: 10}},
   156  		Lists:           map[string]ListSelector{"bar": {MinAvailable: 2}},
   157  	}
   158  	s.ApplyDefaults()
   159  	assert.Equal(t, state, *s.GameServerState)
   160  	assert.Equal(t, int64(10), s.Players.MinAvailable)
   161  	assert.Equal(t, int64(20), s.Players.MaxAvailable)
   162  	assert.Equal(t, int64(0), s.Counters["foo"].MinCount)
   163  	assert.Equal(t, int64(0), s.Counters["foo"].MaxCount)
   164  	assert.Equal(t, int64(1), s.Counters["foo"].MinAvailable)
   165  	assert.Equal(t, int64(10), s.Counters["foo"].MaxAvailable)
   166  	assert.Equal(t, int64(2), s.Lists["bar"].MinAvailable)
   167  	assert.Equal(t, int64(0), s.Lists["bar"].MaxAvailable)
   168  	assert.Equal(t, "", s.Lists["bar"].ContainsValue)
   169  
   170  	// Test apply defaults is idempotent -- calling ApplyDefaults more than one time does not change the original result.
   171  	s.ApplyDefaults()
   172  	assert.Equal(t, state, *s.GameServerState)
   173  	assert.Equal(t, int64(10), s.Players.MinAvailable)
   174  	assert.Equal(t, int64(20), s.Players.MaxAvailable)
   175  	assert.Equal(t, int64(0), s.Counters["foo"].MinCount)
   176  	assert.Equal(t, int64(0), s.Counters["foo"].MaxCount)
   177  	assert.Equal(t, int64(1), s.Counters["foo"].MinAvailable)
   178  	assert.Equal(t, int64(10), s.Counters["foo"].MaxAvailable)
   179  	assert.Equal(t, int64(2), s.Lists["bar"].MinAvailable)
   180  	assert.Equal(t, int64(0), s.Lists["bar"].MaxAvailable)
   181  	assert.Equal(t, "", s.Lists["bar"].ContainsValue)
   182  }
   183  
   184  func TestGameServerSelectorValidate(t *testing.T) {
   185  	t.Parallel()
   186  
   187  	runtime.FeatureTestMutex.Lock()
   188  	defer runtime.FeatureTestMutex.Unlock()
   189  
   190  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s=true", runtime.FeaturePlayerAllocationFilter, runtime.FeatureCountsAndLists)))
   191  
   192  	allocated := agonesv1.GameServerStateAllocated
   193  	starting := agonesv1.GameServerStateStarting
   194  
   195  	fixtures := map[string]struct {
   196  		selector *GameServerSelector
   197  		want     field.ErrorList
   198  	}{
   199  		"valid": {
   200  			selector: &GameServerSelector{GameServerState: &allocated, Players: &PlayerSelector{
   201  				MinAvailable: 0,
   202  				MaxAvailable: 10,
   203  			}},
   204  		},
   205  		"nil values": {
   206  			selector: &GameServerSelector{},
   207  		},
   208  		"invalid state": {
   209  			selector: &GameServerSelector{
   210  				GameServerState: &starting,
   211  			},
   212  			want: field.ErrorList{
   213  				field.Invalid(field.NewPath("fieldName.gameServerState"), starting, "GameServerState must be either Allocated or Ready"),
   214  			},
   215  		},
   216  		"invalid min value": {
   217  			selector: &GameServerSelector{
   218  				Players: &PlayerSelector{
   219  					MinAvailable: -10,
   220  				},
   221  			},
   222  			want: field.ErrorList{
   223  				field.Invalid(field.NewPath("fieldName", "players", "minAvailable"), int64(-10), "must be greater than or equal to 0"),
   224  			},
   225  		},
   226  		"invalid max value": {
   227  			selector: &GameServerSelector{
   228  				Players: &PlayerSelector{
   229  					MinAvailable: -30,
   230  					MaxAvailable: -20,
   231  				},
   232  			},
   233  			want: field.ErrorList{
   234  				field.Invalid(field.NewPath("fieldName", "players", "minAvailable"), int64(-30), "must be greater than or equal to 0"),
   235  				field.Invalid(field.NewPath("fieldName", "players", "maxAvailable"), int64(-20), "must be greater than or equal to 0"),
   236  			},
   237  		},
   238  		"invalid min/max value": {
   239  			selector: &GameServerSelector{
   240  				Players: &PlayerSelector{
   241  					MinAvailable: 10,
   242  					MaxAvailable: 5,
   243  				},
   244  			},
   245  			want: field.ErrorList{
   246  				field.Invalid(field.NewPath("fieldName", "players", "minAvailable"), int64(10), "minAvailable cannot be greater than maxAvailable"),
   247  			},
   248  		},
   249  		"invalid label keys": {
   250  			selector: &GameServerSelector{
   251  				LabelSelector: metav1.LabelSelector{
   252  					MatchLabels: map[string]string{"$$$$": "true"},
   253  				},
   254  			},
   255  			want: field.ErrorList{
   256  				field.Invalid(
   257  					field.NewPath("fieldName", "labelSelector"),
   258  					metav1.LabelSelector{MatchLabels: map[string]string{"$$$$": "true"}},
   259  					`Error converting label selector: key: Invalid value: "$$$$": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`,
   260  				),
   261  			},
   262  		},
   263  		"invalid min/max Counter available value": {
   264  			selector: &GameServerSelector{
   265  				Counters: map[string]CounterSelector{
   266  					"counter": {
   267  						MinAvailable: -1,
   268  						MaxAvailable: -1,
   269  					},
   270  				},
   271  			},
   272  			want: field.ErrorList{
   273  				field.Invalid(field.NewPath("fieldName", "counters[counter]", "minAvailable"), int64(-1), "must be greater than or equal to 0"),
   274  				field.Invalid(field.NewPath("fieldName", "counters[counter]", "maxAvailable"), int64(-1), "must be greater than or equal to 0"),
   275  			},
   276  		},
   277  		"invalid max less than min Counter available value": {
   278  			selector: &GameServerSelector{
   279  				Counters: map[string]CounterSelector{
   280  					"foo": {
   281  						MinAvailable: 10,
   282  						MaxAvailable: 1,
   283  					},
   284  				},
   285  			},
   286  			want: field.ErrorList{
   287  				field.Invalid(field.NewPath("fieldName", "counters[foo]"), int64(1), "maxAvailable must zero or greater than minAvailable 10"),
   288  			},
   289  		},
   290  		"invalid min/max Counter count value": {
   291  			selector: &GameServerSelector{
   292  				Counters: map[string]CounterSelector{
   293  					"counter": {
   294  						MinCount: -1,
   295  						MaxCount: -1,
   296  					},
   297  				},
   298  			},
   299  			want: field.ErrorList{
   300  				field.Invalid(field.NewPath("fieldName", "counters[counter]", "minCount"), int64(-1), "must be greater than or equal to 0"),
   301  				field.Invalid(field.NewPath("fieldName", "counters[counter]", "maxCount"), int64(-1), "must be greater than or equal to 0"),
   302  			},
   303  		},
   304  		"invalid max less than min Counter count value": {
   305  			selector: &GameServerSelector{
   306  				Counters: map[string]CounterSelector{
   307  					"foo": {
   308  						MinCount: 10,
   309  						MaxCount: 1,
   310  					},
   311  				},
   312  			},
   313  			want: field.ErrorList{
   314  				field.Invalid(field.NewPath("fieldName", "counters[foo]"), int64(1), "maxCount must zero or greater than minCount 10"),
   315  			},
   316  		},
   317  		"invalid min/max List value": {
   318  			selector: &GameServerSelector{
   319  				Lists: map[string]ListSelector{
   320  					"list": {
   321  						MinAvailable: -11,
   322  						MaxAvailable: -11,
   323  					},
   324  				},
   325  			},
   326  			want: field.ErrorList{
   327  				field.Invalid(field.NewPath("fieldName", "lists[list]", "minAvailable"), int64(-11), "must be greater than or equal to 0"),
   328  				field.Invalid(field.NewPath("fieldName", "lists[list]", "maxAvailable"), int64(-11), "must be greater than or equal to 0"),
   329  			},
   330  		},
   331  		"invalid max less than min List value": {
   332  			selector: &GameServerSelector{
   333  				Lists: map[string]ListSelector{
   334  					"list": {
   335  						MinAvailable: 11,
   336  						MaxAvailable: 2,
   337  					},
   338  				},
   339  			},
   340  			want: field.ErrorList{
   341  				field.Invalid(field.NewPath("fieldName", "lists[list]"), int64(2), "maxAvailable must zero or greater than minAvailable 11"),
   342  			},
   343  		},
   344  	}
   345  
   346  	for k, v := range fixtures {
   347  		t.Run(k, func(t *testing.T) {
   348  			v.selector.ApplyDefaults()
   349  			allErrs := v.selector.Validate(field.NewPath("fieldName"))
   350  			assert.ElementsMatch(t, v.want, allErrs)
   351  		})
   352  	}
   353  }
   354  
   355  func TestMetaPatchValidate(t *testing.T) {
   356  	t.Parallel()
   357  
   358  	// valid
   359  	mp := &MetaPatch{
   360  		Labels:      nil,
   361  		Annotations: nil,
   362  	}
   363  	path := field.NewPath("spec", "metadata")
   364  	allErrs := mp.Validate(path)
   365  	assert.Len(t, allErrs, 0)
   366  
   367  	mp.Labels = map[string]string{}
   368  	mp.Annotations = map[string]string{}
   369  	allErrs = mp.Validate(path)
   370  	assert.Len(t, allErrs, 0)
   371  
   372  	mp.Labels["foo"] = "bar"
   373  	mp.Annotations["bar"] = "foo"
   374  	allErrs = mp.Validate(path)
   375  	assert.Len(t, allErrs, 0)
   376  
   377  	// invalid label
   378  	invalid := mp.DeepCopy()
   379  	invalid.Labels["$$$$"] = "no"
   380  	allErrs = invalid.Validate(path)
   381  	assert.Len(t, allErrs, 1)
   382  	assert.Equal(t, "spec.metadata.labels", allErrs[0].Field)
   383  
   384  	// invalid annotation
   385  	invalid = mp.DeepCopy()
   386  	invalid.Annotations["$$$$"] = "no"
   387  
   388  	allErrs = invalid.Validate(path)
   389  	require.Len(t, allErrs, 1)
   390  	assert.Equal(t, "spec.metadata.annotations", allErrs[0].Field)
   391  
   392  	// invalid both
   393  	invalid.Labels["$$$$"] = "no"
   394  	allErrs = invalid.Validate(path)
   395  	require.Len(t, allErrs, 2)
   396  	assert.Equal(t, "spec.metadata.labels", allErrs[0].Field)
   397  	assert.Equal(t, "spec.metadata.annotations", allErrs[1].Field)
   398  }
   399  
   400  func TestGameServerSelectorMatches(t *testing.T) {
   401  	t.Parallel()
   402  
   403  	runtime.FeatureTestMutex.Lock()
   404  	defer runtime.FeatureTestMutex.Unlock()
   405  
   406  	blueSelector := metav1.LabelSelector{
   407  		MatchLabels: map[string]string{"colour": "blue"},
   408  	}
   409  
   410  	allocatedState := agonesv1.GameServerStateAllocated
   411  	fixtures := map[string]struct {
   412  		features   string
   413  		selector   *GameServerSelector
   414  		gameServer *agonesv1.GameServer
   415  		matches    bool
   416  	}{
   417  		"no labels, pass": {
   418  			selector:   &GameServerSelector{},
   419  			gameServer: &agonesv1.GameServer{},
   420  			matches:    true,
   421  		},
   422  
   423  		"no labels, fail": {
   424  			selector: &GameServerSelector{
   425  				LabelSelector: blueSelector,
   426  			},
   427  			gameServer: &agonesv1.GameServer{},
   428  			matches:    false,
   429  		},
   430  		"single label, match": {
   431  			selector: &GameServerSelector{
   432  				LabelSelector: blueSelector,
   433  			},
   434  			gameServer: &agonesv1.GameServer{
   435  				ObjectMeta: metav1.ObjectMeta{
   436  					Labels: map[string]string{"colour": "blue"},
   437  				},
   438  			},
   439  			matches: true,
   440  		},
   441  		"single label, fail": {
   442  			selector: &GameServerSelector{
   443  				LabelSelector: blueSelector,
   444  			},
   445  			gameServer: &agonesv1.GameServer{
   446  				ObjectMeta: metav1.ObjectMeta{
   447  					Labels: map[string]string{"colour": "purple"},
   448  				},
   449  			},
   450  			matches: false,
   451  		},
   452  		"two labels, pass": {
   453  			selector: &GameServerSelector{
   454  				LabelSelector: metav1.LabelSelector{
   455  					MatchLabels: map[string]string{"colour": "blue", "animal": "frog"},
   456  				},
   457  			},
   458  			gameServer: &agonesv1.GameServer{
   459  				ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"colour": "blue", "animal": "frog"}},
   460  			},
   461  			matches: true,
   462  		},
   463  		"two labels, fail": {
   464  			selector: &GameServerSelector{
   465  				LabelSelector: metav1.LabelSelector{
   466  					MatchLabels: map[string]string{"colour": "blue", "animal": "cat"},
   467  				},
   468  			},
   469  			gameServer: &agonesv1.GameServer{
   470  				ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"colour": "blue", "animal": "frog"}},
   471  			},
   472  			matches: false,
   473  		},
   474  		"state filter, pass": {
   475  			selector: &GameServerSelector{
   476  				GameServerState: &allocatedState,
   477  			},
   478  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{State: allocatedState}},
   479  			matches:    true,
   480  		},
   481  		"state filter, fail": {
   482  			selector: &GameServerSelector{
   483  				GameServerState: &allocatedState,
   484  			},
   485  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateReady}},
   486  			matches:    false,
   487  		},
   488  		"player tracking, between, pass": {
   489  			features: string(runtime.FeaturePlayerAllocationFilter) + "=true",
   490  			selector: &GameServerSelector{Players: &PlayerSelector{
   491  				MinAvailable: 10,
   492  				MaxAvailable: 20,
   493  			}},
   494  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   495  				Players: &agonesv1.PlayerStatus{
   496  					Count:    20,
   497  					Capacity: 35,
   498  				},
   499  			}},
   500  			matches: true,
   501  		},
   502  		"player tracking, between, fail": {
   503  			features: string(runtime.FeaturePlayerAllocationFilter) + "=true",
   504  			selector: &GameServerSelector{Players: &PlayerSelector{
   505  				MinAvailable: 10,
   506  				MaxAvailable: 20,
   507  			}},
   508  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   509  				Players: &agonesv1.PlayerStatus{
   510  					Count:    30,
   511  					Capacity: 35,
   512  				},
   513  			}},
   514  			matches: false,
   515  		},
   516  		"player tracking, max, pass": {
   517  			features: string(runtime.FeaturePlayerAllocationFilter) + "=true",
   518  			selector: &GameServerSelector{Players: &PlayerSelector{
   519  				MinAvailable: 10,
   520  			}},
   521  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   522  				Players: &agonesv1.PlayerStatus{
   523  					Count:    20,
   524  					Capacity: 35,
   525  				},
   526  			}},
   527  			matches: true,
   528  		},
   529  		"player tracking, max, fail": {
   530  			features: string(runtime.FeaturePlayerAllocationFilter) + "=true",
   531  			selector: &GameServerSelector{Players: &PlayerSelector{
   532  				MinAvailable: 10,
   533  			}},
   534  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   535  				Players: &agonesv1.PlayerStatus{
   536  					Count:    30,
   537  					Capacity: 35,
   538  				},
   539  			}},
   540  			matches: true,
   541  		},
   542  		"combo": {
   543  			features: string(runtime.FeaturePlayerAllocationFilter) + "=true&",
   544  			selector: &GameServerSelector{
   545  				LabelSelector: metav1.LabelSelector{
   546  					MatchLabels: map[string]string{"colour": "blue"},
   547  				},
   548  				GameServerState: &allocatedState,
   549  				Players: &PlayerSelector{
   550  					MinAvailable: 10,
   551  					MaxAvailable: 20,
   552  				},
   553  			},
   554  			gameServer: &agonesv1.GameServer{
   555  				ObjectMeta: metav1.ObjectMeta{
   556  					Labels: map[string]string{"colour": "blue"},
   557  				},
   558  				Status: agonesv1.GameServerStatus{
   559  					State: allocatedState,
   560  					Players: &agonesv1.PlayerStatus{
   561  						Count:    5,
   562  						Capacity: 25,
   563  					},
   564  				},
   565  			},
   566  			matches: true,
   567  		},
   568  		"Counter has available capacity": {
   569  			features: string(runtime.FeatureCountsAndLists) + "=true",
   570  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   571  				"sessions": {
   572  					MinAvailable: 1,
   573  					MaxAvailable: 1000,
   574  				},
   575  			}},
   576  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   577  				Counters: map[string]agonesv1.CounterStatus{
   578  					"sessions": {
   579  						Count:    10,
   580  						Capacity: 1000,
   581  					},
   582  				},
   583  			}},
   584  			matches: true,
   585  		},
   586  		"Counter has below minimum available capacity": {
   587  			features: string(runtime.FeatureCountsAndLists) + "=true",
   588  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   589  				"players": {
   590  					MinAvailable: 100,
   591  					MaxAvailable: 0,
   592  				},
   593  			}},
   594  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   595  				Counters: map[string]agonesv1.CounterStatus{
   596  					"players": {
   597  						Count:    999,
   598  						Capacity: 1000,
   599  					},
   600  				},
   601  			}},
   602  			matches: false,
   603  		},
   604  		"Counter has above maximum available capacity": {
   605  			features: string(runtime.FeatureCountsAndLists) + "=true",
   606  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   607  				"animals": {
   608  					MinAvailable: 1,
   609  					MaxAvailable: 100,
   610  				},
   611  			}},
   612  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   613  				Counters: map[string]agonesv1.CounterStatus{
   614  					"animals": {
   615  						Count:    0,
   616  						Capacity: 1000,
   617  					},
   618  				},
   619  			}},
   620  			matches: false,
   621  		},
   622  		"Counter has count in requested range (MaxCount undefined = 0 = unlimited)": {
   623  			features: string(runtime.FeatureCountsAndLists) + "=true",
   624  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   625  				"games": {
   626  					MinCount: 1,
   627  				},
   628  			}},
   629  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   630  				Counters: map[string]agonesv1.CounterStatus{
   631  					"games": {
   632  						Count:    10,
   633  						Capacity: 1000,
   634  					},
   635  				},
   636  			}},
   637  			matches: true,
   638  		},
   639  		"Counter has count below minimum": {
   640  			features: string(runtime.FeatureCountsAndLists) + "=true",
   641  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   642  				"characters": {
   643  					MinCount: 1,
   644  					MaxCount: 0,
   645  				},
   646  			}},
   647  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   648  				Counters: map[string]agonesv1.CounterStatus{
   649  					"characters": {
   650  						Count:    0,
   651  						Capacity: 100,
   652  					},
   653  				},
   654  			}},
   655  			matches: false,
   656  		},
   657  		"Counter has count above maximum": {
   658  			features: string(runtime.FeatureCountsAndLists) + "=true",
   659  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   660  				"monsters": {
   661  					MinCount: 0,
   662  					MaxCount: 10,
   663  				},
   664  			}},
   665  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   666  				Counters: map[string]agonesv1.CounterStatus{
   667  					"monsters": {
   668  						Count:    11,
   669  						Capacity: 100,
   670  					},
   671  				},
   672  			}},
   673  			matches: false,
   674  		},
   675  		"Counter does not exist": {
   676  			features: string(runtime.FeatureCountsAndLists) + "=true",
   677  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   678  				"dragoons": {
   679  					MinCount: 1,
   680  					MaxCount: 10,
   681  				},
   682  			}},
   683  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   684  				Counters: map[string]agonesv1.CounterStatus{
   685  					"dragons": {
   686  						Count:    1,
   687  						Capacity: 100,
   688  					},
   689  				},
   690  			}},
   691  			matches: false,
   692  		},
   693  		"GameServer does not have Counters": {
   694  			features: string(runtime.FeatureCountsAndLists) + "=true",
   695  			selector: &GameServerSelector{Counters: map[string]CounterSelector{
   696  				"dragoons": {
   697  					MinCount: 1,
   698  					MaxCount: 10,
   699  				},
   700  			}},
   701  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   702  				Lists: map[string]agonesv1.ListStatus{
   703  					"bazzles": {
   704  						Capacity: 3,
   705  						Values:   []string{"baz1", "baz2", "baz3"},
   706  					},
   707  				},
   708  			}},
   709  			matches: false,
   710  		},
   711  		"List has available capacity": {
   712  			features: string(runtime.FeatureCountsAndLists) + "=true",
   713  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   714  				"lobbies": {
   715  					MinAvailable: 1,
   716  					MaxAvailable: 3,
   717  				},
   718  			}},
   719  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   720  				Lists: map[string]agonesv1.ListStatus{
   721  					"lobbies": {
   722  						Capacity: 3,
   723  						Values:   []string{"lobby1", "lobby2"},
   724  					},
   725  				},
   726  			}},
   727  			matches: true,
   728  		},
   729  		"List has below minimum available capacity": {
   730  			features: string(runtime.FeatureCountsAndLists) + "=true",
   731  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   732  				"avatars": {
   733  					MinAvailable: 1,
   734  					MaxAvailable: 1000,
   735  				},
   736  			}},
   737  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   738  				Lists: map[string]agonesv1.ListStatus{
   739  					"avatars": {
   740  						Capacity: 3,
   741  						Values:   []string{"avatar1", "avatar2", "avatar3"},
   742  					},
   743  				},
   744  			}},
   745  			matches: false,
   746  		},
   747  		"List has above maximum available capacity": {
   748  			features: string(runtime.FeatureCountsAndLists) + "=true",
   749  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   750  				"things": {
   751  					MinAvailable: 1,
   752  					MaxAvailable: 10,
   753  				},
   754  			}},
   755  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   756  				Lists: map[string]agonesv1.ListStatus{
   757  					"things": {
   758  						Capacity: 1000,
   759  						Values:   []string{"thing1", "thing2", "thing3"},
   760  					},
   761  				},
   762  			}},
   763  			matches: false,
   764  		},
   765  		"List does not exist": {
   766  			features: string(runtime.FeatureCountsAndLists) + "=true",
   767  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   768  				"thingamabobs": {
   769  					MinAvailable: 1,
   770  					MaxAvailable: 100,
   771  				},
   772  			}},
   773  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   774  				Lists: map[string]agonesv1.ListStatus{
   775  					"thingamajigs": {
   776  						Capacity: 100,
   777  						Values:   []string{"thingamajig1", "thingamajig2"},
   778  					},
   779  				},
   780  			}},
   781  			matches: false,
   782  		},
   783  		"List contains value": {
   784  			features: string(runtime.FeatureCountsAndLists) + "=true",
   785  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   786  				"bazzles": {
   787  					ContainsValue: "baz1",
   788  				},
   789  			}},
   790  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   791  				Lists: map[string]agonesv1.ListStatus{
   792  					"bazzles": {
   793  						Capacity: 3,
   794  						Values:   []string{"baz1", "baz2", "baz3"},
   795  					},
   796  				},
   797  			}},
   798  			matches: true,
   799  		},
   800  		"List does not contain value": {
   801  			features: string(runtime.FeatureCountsAndLists) + "=true",
   802  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   803  				"bazzles": {
   804  					ContainsValue: "BAZ1",
   805  				},
   806  			}},
   807  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   808  				Lists: map[string]agonesv1.ListStatus{
   809  					"bazzles": {
   810  						Capacity: 3,
   811  						Values:   []string{"baz1", "baz2", "baz3"},
   812  					},
   813  				},
   814  			}},
   815  			matches: false,
   816  		},
   817  		"GameServer does not have Lists": {
   818  			features: string(runtime.FeatureCountsAndLists) + "=true",
   819  			selector: &GameServerSelector{Lists: map[string]ListSelector{
   820  				"bazzles": {
   821  					ContainsValue: "BAZ1",
   822  				},
   823  			}},
   824  			gameServer: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   825  				Counters: map[string]agonesv1.CounterStatus{
   826  					"dragons": {
   827  						Count:    1,
   828  						Capacity: 100,
   829  					},
   830  				},
   831  			}},
   832  			matches: false,
   833  		},
   834  	}
   835  
   836  	for k, v := range fixtures {
   837  		t.Run(k, func(t *testing.T) {
   838  			if v.features != "" {
   839  				require.NoError(t, runtime.ParseFeatures(v.features))
   840  			}
   841  
   842  			match := v.selector.Matches(v.gameServer)
   843  			assert.Equal(t, v.matches, match)
   844  		})
   845  	}
   846  }
   847  
   848  // Helper function for creating int64 pointers
   849  func int64Pointer(x int64) *int64 {
   850  	return &x
   851  }
   852  
   853  func TestGameServerCounterActions(t *testing.T) {
   854  	t.Parallel()
   855  
   856  	runtime.FeatureTestMutex.Lock()
   857  	defer runtime.FeatureTestMutex.Unlock()
   858  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
   859  
   860  	DECREMENT := "Decrement"
   861  	INCREMENT := "Increment"
   862  
   863  	testScenarios := map[string]struct {
   864  		ca      CounterAction
   865  		counter string
   866  		gs      *agonesv1.GameServer
   867  		want    *agonesv1.GameServer
   868  		wantErr bool
   869  	}{
   870  		"update counter capacity and count is set to capacity": {
   871  			ca: CounterAction{
   872  				Capacity: int64Pointer(0),
   873  			},
   874  			counter: "mages",
   875  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   876  				Counters: map[string]agonesv1.CounterStatus{
   877  					"mages": {
   878  						Count:    1,
   879  						Capacity: 100,
   880  					}}}},
   881  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   882  				Counters: map[string]agonesv1.CounterStatus{
   883  					"mages": {
   884  						Count:    0,
   885  						Capacity: 0,
   886  					}}}},
   887  			wantErr: false,
   888  		},
   889  		"fail update counter capacity and truncate update count": {
   890  			ca: CounterAction{
   891  				Action:   &INCREMENT,
   892  				Amount:   int64Pointer(10),
   893  				Capacity: int64Pointer(-1),
   894  			},
   895  			counter: "sages",
   896  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   897  				Counters: map[string]agonesv1.CounterStatus{
   898  					"sages": {
   899  						Count:    99,
   900  						Capacity: 100,
   901  					}}}},
   902  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   903  				Counters: map[string]agonesv1.CounterStatus{
   904  					"sages": {
   905  						Count:    100,
   906  						Capacity: 100,
   907  					}}}},
   908  			wantErr: true,
   909  		},
   910  		"update counter count": {
   911  			ca: CounterAction{
   912  				Action: &INCREMENT,
   913  				Amount: int64Pointer(10),
   914  			},
   915  			counter: "baddies",
   916  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   917  				Counters: map[string]agonesv1.CounterStatus{
   918  					"baddies": {
   919  						Count:    1,
   920  						Capacity: 100,
   921  					}}}},
   922  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   923  				Counters: map[string]agonesv1.CounterStatus{
   924  					"baddies": {
   925  						Count:    11,
   926  						Capacity: 100,
   927  					}}}},
   928  			wantErr: false,
   929  		},
   930  		"update counter count and capacity": {
   931  			ca: CounterAction{
   932  				Action:   &DECREMENT,
   933  				Amount:   int64Pointer(10),
   934  				Capacity: int64Pointer(10),
   935  			},
   936  			counter: "heroes",
   937  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   938  				Counters: map[string]agonesv1.CounterStatus{
   939  					"heroes": {
   940  						Count:    11,
   941  						Capacity: 100,
   942  					}}}},
   943  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   944  				Counters: map[string]agonesv1.CounterStatus{
   945  					"heroes": {
   946  						// Note: The Capacity is set first, and Count updated to not be greater than Capacity.
   947  						// Then the Count is decremented. See: gameserver.go/UpdateCounterCapacity
   948  						Count:    0,
   949  						Capacity: 10,
   950  					}}}},
   951  			wantErr: false,
   952  		},
   953  	}
   954  
   955  	for test, testScenario := range testScenarios {
   956  		t.Run(test, func(t *testing.T) {
   957  			errs := testScenario.ca.CounterActions(testScenario.counter, testScenario.gs)
   958  			if errs != nil {
   959  				assert.True(t, testScenario.wantErr)
   960  			} else {
   961  				assert.False(t, testScenario.wantErr)
   962  			}
   963  			assert.Equal(t, testScenario.want, testScenario.gs)
   964  		})
   965  	}
   966  }
   967  
   968  func TestGameServerListActions(t *testing.T) {
   969  	t.Parallel()
   970  
   971  	runtime.FeatureTestMutex.Lock()
   972  	defer runtime.FeatureTestMutex.Unlock()
   973  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
   974  
   975  	testScenarios := map[string]struct {
   976  		la      ListAction
   977  		list    string
   978  		gs      *agonesv1.GameServer
   979  		want    *agonesv1.GameServer
   980  		wantErr bool
   981  	}{
   982  		"update list capacity truncates list": {
   983  			la: ListAction{
   984  				Capacity: int64Pointer(0),
   985  			},
   986  			list: "pages",
   987  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   988  				Lists: map[string]agonesv1.ListStatus{
   989  					"pages": {
   990  						Values:   []string{"page1", "page2"},
   991  						Capacity: 100,
   992  					}}}},
   993  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
   994  				Lists: map[string]agonesv1.ListStatus{
   995  					"pages": {
   996  						Values:   []string{},
   997  						Capacity: 0,
   998  					}}}},
   999  			wantErr: false,
  1000  		},
  1001  		"update list values": {
  1002  			la: ListAction{
  1003  				AddValues: []string{"sage1", "sage3"},
  1004  			},
  1005  			list: "sages",
  1006  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
  1007  				Lists: map[string]agonesv1.ListStatus{
  1008  					"sages": {
  1009  						Values:   []string{"sage1", "sage2"},
  1010  						Capacity: 100,
  1011  					}}}},
  1012  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
  1013  				Lists: map[string]agonesv1.ListStatus{
  1014  					"sages": {
  1015  						Values:   []string{"sage1", "sage2", "sage3"},
  1016  						Capacity: 100,
  1017  					}}}},
  1018  			wantErr: false,
  1019  		},
  1020  		"update list values and capacity": {
  1021  			la: ListAction{
  1022  				AddValues:    []string{"magician1", "magician3"},
  1023  				Capacity:     int64Pointer(42),
  1024  				DeleteValues: []string{"magician2", "magician5", "magician6"},
  1025  			},
  1026  			list: "magicians",
  1027  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
  1028  				Lists: map[string]agonesv1.ListStatus{
  1029  					"magicians": {
  1030  						Values:   []string{"magician1", "magician2", "magician4", "magician5"},
  1031  						Capacity: 100,
  1032  					}}}},
  1033  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
  1034  				Lists: map[string]agonesv1.ListStatus{
  1035  					"magicians": {
  1036  						Values:   []string{"magician1", "magician4", "magician3"},
  1037  						Capacity: 42,
  1038  					}}}},
  1039  			wantErr: false,
  1040  		},
  1041  		"update list values and capacity - value add truncates silently": {
  1042  			la: ListAction{
  1043  				AddValues: []string{"fairy1", "fairy3"},
  1044  				Capacity:  int64Pointer(2),
  1045  			},
  1046  			list: "fairies",
  1047  			gs: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
  1048  				Lists: map[string]agonesv1.ListStatus{
  1049  					"fairies": {
  1050  						Values:   []string{"fairy1", "fairy2"},
  1051  						Capacity: 100,
  1052  					}}}},
  1053  			want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{
  1054  				Lists: map[string]agonesv1.ListStatus{
  1055  					"fairies": {
  1056  						Values:   []string{"fairy1", "fairy2"},
  1057  						Capacity: 2,
  1058  					}}}},
  1059  			wantErr: false,
  1060  		},
  1061  	}
  1062  
  1063  	for test, testScenario := range testScenarios {
  1064  		t.Run(test, func(t *testing.T) {
  1065  			errs := testScenario.la.ListActions(testScenario.list, testScenario.gs)
  1066  			if errs != nil {
  1067  				assert.True(t, testScenario.wantErr)
  1068  			} else {
  1069  				assert.False(t, testScenario.wantErr)
  1070  			}
  1071  			assert.Equal(t, testScenario.want, testScenario.gs)
  1072  		})
  1073  	}
  1074  }
  1075  
  1076  func TestValidatePriorities(t *testing.T) {
  1077  	t.Parallel()
  1078  
  1079  	runtime.FeatureTestMutex.Lock()
  1080  	defer runtime.FeatureTestMutex.Unlock()
  1081  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
  1082  
  1083  	fieldPath := field.NewPath("spec.Priorities")
  1084  
  1085  	testScenarios := map[string]struct {
  1086  		priorities []agonesv1.Priority
  1087  		wantErr    bool
  1088  	}{
  1089  		"Valid priorities": {
  1090  			priorities: []agonesv1.Priority{
  1091  				{
  1092  					Type:  agonesv1.GameServerPriorityList,
  1093  					Key:   "test",
  1094  					Order: agonesv1.GameServerPriorityAscending,
  1095  				},
  1096  				{
  1097  					Type:  agonesv1.GameServerPriorityCounter,
  1098  					Key:   "test",
  1099  					Order: agonesv1.GameServerPriorityDescending,
  1100  				},
  1101  			},
  1102  			wantErr: false,
  1103  		},
  1104  		"No type": {
  1105  			priorities: []agonesv1.Priority{
  1106  				{
  1107  					Key:   "test",
  1108  					Order: agonesv1.GameServerPriorityDescending,
  1109  				},
  1110  			},
  1111  			wantErr: true,
  1112  		},
  1113  		"Invalid type": {
  1114  			priorities: []agonesv1.Priority{
  1115  				{
  1116  					Key:   "test",
  1117  					Type:  "invalid",
  1118  					Order: agonesv1.GameServerPriorityDescending,
  1119  				},
  1120  			},
  1121  			wantErr: true,
  1122  		},
  1123  		"No Key": {
  1124  			priorities: []agonesv1.Priority{
  1125  				{
  1126  					Type:  agonesv1.GameServerPriorityCounter,
  1127  					Order: agonesv1.GameServerPriorityDescending,
  1128  				},
  1129  			},
  1130  			wantErr: true,
  1131  		},
  1132  		"No Order": {
  1133  			priorities: []agonesv1.Priority{
  1134  				{
  1135  					Type: agonesv1.GameServerPriorityList,
  1136  					Key:  "test",
  1137  				},
  1138  			},
  1139  			wantErr: true,
  1140  		},
  1141  		"Invalid Order": {
  1142  			priorities: []agonesv1.Priority{
  1143  				{
  1144  					Type:  agonesv1.GameServerPriorityList,
  1145  					Key:   "test",
  1146  					Order: "invalid",
  1147  				},
  1148  			},
  1149  			wantErr: true,
  1150  		},
  1151  	}
  1152  
  1153  	for test, testScenario := range testScenarios {
  1154  		t.Run(test, func(t *testing.T) {
  1155  			allErrs := validatePriorities(testScenario.priorities, fieldPath)
  1156  			if testScenario.wantErr {
  1157  				assert.NotNil(t, allErrs)
  1158  			} else {
  1159  				assert.Nil(t, allErrs)
  1160  			}
  1161  		})
  1162  	}
  1163  }
  1164  
  1165  func TestValidateCounterActions(t *testing.T) {
  1166  	t.Parallel()
  1167  
  1168  	runtime.FeatureTestMutex.Lock()
  1169  	defer runtime.FeatureTestMutex.Unlock()
  1170  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
  1171  
  1172  	fieldPath := field.NewPath("spec.Counters")
  1173  	decrement := agonesv1.GameServerPriorityDecrement
  1174  	increment := agonesv1.GameServerPriorityIncrement
  1175  
  1176  	testScenarios := map[string]struct {
  1177  		counterActions map[string]CounterAction
  1178  		wantErr        bool
  1179  	}{
  1180  		"Valid CounterActions": {
  1181  			counterActions: map[string]CounterAction{
  1182  				"foo": {
  1183  					Action: &increment,
  1184  					Amount: int64Pointer(10),
  1185  				},
  1186  				"bar": {
  1187  					Capacity: int64Pointer(100),
  1188  				},
  1189  				"baz": {
  1190  					Action:   &decrement,
  1191  					Amount:   int64Pointer(1000),
  1192  					Capacity: int64Pointer(0),
  1193  				},
  1194  			},
  1195  			wantErr: false,
  1196  		},
  1197  		"Negative Amount": {
  1198  			counterActions: map[string]CounterAction{
  1199  				"foo": {
  1200  					Action: &increment,
  1201  					Amount: int64Pointer(-1),
  1202  				},
  1203  			},
  1204  			wantErr: true,
  1205  		},
  1206  		"Negative Capacity": {
  1207  			counterActions: map[string]CounterAction{
  1208  				"foo": {
  1209  					Capacity: int64Pointer(-20),
  1210  				},
  1211  			},
  1212  			wantErr: true,
  1213  		},
  1214  		"Amount but no Action": {
  1215  			counterActions: map[string]CounterAction{
  1216  				"foo": {
  1217  					Amount: int64Pointer(10),
  1218  				},
  1219  			},
  1220  			wantErr: true,
  1221  		},
  1222  		"Action but no Amount": {
  1223  			counterActions: map[string]CounterAction{
  1224  				"foo": {
  1225  					Action: &decrement,
  1226  				},
  1227  			},
  1228  			wantErr: true,
  1229  		},
  1230  	}
  1231  
  1232  	for test, testScenario := range testScenarios {
  1233  		t.Run(test, func(t *testing.T) {
  1234  			allErrs := validateCounterActions(testScenario.counterActions, fieldPath)
  1235  			if testScenario.wantErr {
  1236  				assert.NotNil(t, allErrs)
  1237  			} else {
  1238  				assert.Nil(t, allErrs)
  1239  			}
  1240  		})
  1241  	}
  1242  }
  1243  
  1244  func TestValidateListActions(t *testing.T) {
  1245  	t.Parallel()
  1246  
  1247  	runtime.FeatureTestMutex.Lock()
  1248  	defer runtime.FeatureTestMutex.Unlock()
  1249  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
  1250  
  1251  	fieldPath := field.NewPath("spec.Lists")
  1252  
  1253  	testScenarios := map[string]struct {
  1254  		listActions map[string]ListAction
  1255  		wantErr     bool
  1256  	}{
  1257  		"Valid ListActions": {
  1258  			listActions: map[string]ListAction{
  1259  				"foo": {
  1260  					AddValues: []string{"hello", "world"},
  1261  					Capacity:  int64Pointer(10),
  1262  				},
  1263  				"bar": {
  1264  					Capacity: int64Pointer(0),
  1265  				},
  1266  				"baz": {
  1267  					AddValues:    []string{},
  1268  					DeleteValues: []string{"good", "bye"},
  1269  				},
  1270  			},
  1271  			wantErr: false,
  1272  		},
  1273  		"Negative Capacity": {
  1274  			listActions: map[string]ListAction{
  1275  				"foo": {
  1276  					Capacity: int64Pointer(-20),
  1277  				},
  1278  			},
  1279  			wantErr: true,
  1280  		},
  1281  	}
  1282  
  1283  	for test, testScenario := range testScenarios {
  1284  		t.Run(test, func(t *testing.T) {
  1285  			allErrs := validateListActions(testScenario.listActions, fieldPath)
  1286  			if testScenario.wantErr {
  1287  				assert.NotNil(t, allErrs)
  1288  			} else {
  1289  				assert.Nil(t, allErrs)
  1290  			}
  1291  		})
  1292  	}
  1293  }
  1294  
  1295  func TestGameServerAllocationValidate(t *testing.T) {
  1296  	t.Parallel()
  1297  
  1298  	runtime.FeatureTestMutex.Lock()
  1299  	defer runtime.FeatureTestMutex.Unlock()
  1300  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true&%s=false",
  1301  		runtime.FeaturePlayerAllocationFilter,
  1302  		runtime.FeatureCountsAndLists)))
  1303  
  1304  	gsa := &GameServerAllocation{}
  1305  	gsa.ApplyDefaults()
  1306  
  1307  	allErrs := gsa.Validate()
  1308  	assert.Len(t, allErrs, 0)
  1309  
  1310  	gsa.Spec.Scheduling = "FLERG"
  1311  
  1312  	allErrs = gsa.Validate()
  1313  	assert.Len(t, allErrs, 1)
  1314  
  1315  	assert.Equal(t, field.ErrorTypeNotSupported, allErrs[0].Type)
  1316  	assert.Equal(t, "spec.scheduling", allErrs[0].Field)
  1317  
  1318  	// invalid player selection
  1319  	gsa = &GameServerAllocation{
  1320  		Spec: GameServerAllocationSpec{
  1321  			Required: GameServerSelector{
  1322  				Players: &PlayerSelector{
  1323  					MinAvailable: -10,
  1324  				},
  1325  			},
  1326  			Preferred: []GameServerSelector{
  1327  				{Players: &PlayerSelector{MaxAvailable: -10}},
  1328  			},
  1329  			MetaPatch: MetaPatch{
  1330  				Labels: map[string]string{"$$$": "foo"},
  1331  			},
  1332  			Priorities: []agonesv1.Priority{},
  1333  			Counters:   map[string]CounterAction{},
  1334  			Lists:      map[string]ListAction{},
  1335  		},
  1336  	}
  1337  	gsa.ApplyDefaults()
  1338  
  1339  	allErrs = gsa.Validate()
  1340  	sort.Slice(allErrs, func(i, j int) bool {
  1341  		return allErrs[i].Field > allErrs[j].Field
  1342  	})
  1343  	assert.Len(t, allErrs, 7)
  1344  	assert.Equal(t, "spec.required.players.minAvailable", allErrs[0].Field)
  1345  	assert.Equal(t, "spec.priorities", allErrs[1].Field)
  1346  	assert.Equal(t, "spec.preferred[0].players.minAvailable", allErrs[2].Field)
  1347  	assert.Equal(t, "spec.preferred[0].players.maxAvailable", allErrs[3].Field)
  1348  	assert.Equal(t, "spec.metadata.labels", allErrs[4].Field)
  1349  	assert.Equal(t, "spec.lists", allErrs[5].Field)
  1350  	assert.Equal(t, "spec.counters", allErrs[6].Field)
  1351  }
  1352  
  1353  func TestGameServerAllocationConverter(t *testing.T) {
  1354  	t.Parallel()
  1355  
  1356  	gsa := &GameServerAllocation{
  1357  		Spec: GameServerAllocationSpec{
  1358  			Scheduling: "Packed",
  1359  			Required: GameServerSelector{
  1360  				Players: &PlayerSelector{
  1361  					MinAvailable: 5,
  1362  					MaxAvailable: 10,
  1363  				},
  1364  			},
  1365  			Preferred: []GameServerSelector{
  1366  				{Players: &PlayerSelector{MinAvailable: 10,
  1367  					MaxAvailable: 20}},
  1368  			},
  1369  		},
  1370  	}
  1371  	gsaExpected := &GameServerAllocation{
  1372  		Spec: GameServerAllocationSpec{
  1373  			Scheduling: "Packed",
  1374  			Required: GameServerSelector{
  1375  				Players: &PlayerSelector{
  1376  					MinAvailable: 5,
  1377  					MaxAvailable: 10,
  1378  				},
  1379  			},
  1380  			Preferred: []GameServerSelector{
  1381  				{Players: &PlayerSelector{MinAvailable: 10,
  1382  					MaxAvailable: 20}},
  1383  			},
  1384  			Selectors: []GameServerSelector{
  1385  				{Players: &PlayerSelector{MinAvailable: 10,
  1386  					MaxAvailable: 20}},
  1387  				{Players: &PlayerSelector{
  1388  					MinAvailable: 5,
  1389  					MaxAvailable: 10}},
  1390  			},
  1391  		},
  1392  	}
  1393  
  1394  	gsa.Converter()
  1395  	assert.Equal(t, gsaExpected, gsa)
  1396  }
  1397  
  1398  func TestSortKey(t *testing.T) {
  1399  	t.Parallel()
  1400  
  1401  	runtime.FeatureTestMutex.Lock()
  1402  	defer runtime.FeatureTestMutex.Unlock()
  1403  	assert.NoError(t, runtime.ParseFeatures(fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists)))
  1404  
  1405  	gameServerAllocation1 := &GameServerAllocation{
  1406  		Spec: GameServerAllocationSpec{
  1407  			Scheduling: "Packed",
  1408  			Priorities: []agonesv1.Priority{
  1409  				{
  1410  					Type:  "List",
  1411  					Key:   "foo",
  1412  					Order: "Descending",
  1413  				},
  1414  			},
  1415  		},
  1416  	}
  1417  
  1418  	gameServerAllocation2 := &GameServerAllocation{
  1419  		Spec: GameServerAllocationSpec{
  1420  			Selectors: []GameServerSelector{
  1421  				{LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}},
  1422  			},
  1423  			Scheduling: "Packed",
  1424  			Priorities: []agonesv1.Priority{
  1425  				{
  1426  					Type:  "List",
  1427  					Key:   "foo",
  1428  					Order: "Descending",
  1429  				},
  1430  			},
  1431  		},
  1432  	}
  1433  
  1434  	gameServerAllocation3 := &GameServerAllocation{
  1435  		Spec: GameServerAllocationSpec{
  1436  			Scheduling: "Packed",
  1437  			Priorities: []agonesv1.Priority{
  1438  				{
  1439  					Type:  "Counter",
  1440  					Key:   "foo",
  1441  					Order: "Descending",
  1442  				},
  1443  			},
  1444  		},
  1445  	}
  1446  
  1447  	gameServerAllocation4 := &GameServerAllocation{
  1448  		Spec: GameServerAllocationSpec{
  1449  			Scheduling: "Distributed",
  1450  			Priorities: []agonesv1.Priority{
  1451  				{
  1452  					Type:  "List",
  1453  					Key:   "foo",
  1454  					Order: "Descending",
  1455  				},
  1456  			},
  1457  		},
  1458  	}
  1459  
  1460  	gameServerAllocation5 := &GameServerAllocation{}
  1461  
  1462  	gameServerAllocation6 := &GameServerAllocation{
  1463  		Spec: GameServerAllocationSpec{
  1464  			Priorities: []agonesv1.Priority{},
  1465  		},
  1466  	}
  1467  
  1468  	testScenarios := map[string]struct {
  1469  		gsa1      *GameServerAllocation
  1470  		gsa2      *GameServerAllocation
  1471  		wantEqual bool
  1472  	}{
  1473  		"equivalent GameServerAllocation": {
  1474  			gsa1:      gameServerAllocation1,
  1475  			gsa2:      gameServerAllocation2,
  1476  			wantEqual: true,
  1477  		},
  1478  		"different Scheduling GameServerAllocation": {
  1479  			gsa1:      gameServerAllocation1,
  1480  			gsa2:      gameServerAllocation4,
  1481  			wantEqual: false,
  1482  		},
  1483  		"equivalent empty GameServerAllocation": {
  1484  			gsa1:      gameServerAllocation5,
  1485  			gsa2:      gameServerAllocation6,
  1486  			wantEqual: true,
  1487  		},
  1488  		"different Priorities GameServerAllocation": {
  1489  			gsa1:      gameServerAllocation1,
  1490  			gsa2:      gameServerAllocation3,
  1491  			wantEqual: false,
  1492  		},
  1493  	}
  1494  
  1495  	for test, testScenario := range testScenarios {
  1496  		t.Run(test, func(t *testing.T) {
  1497  			key1, err := testScenario.gsa1.SortKey()
  1498  			assert.NoError(t, err)
  1499  			key2, err := testScenario.gsa2.SortKey()
  1500  			assert.NoError(t, err)
  1501  
  1502  			if testScenario.wantEqual {
  1503  				assert.Equal(t, key1, key2)
  1504  			} else {
  1505  				assert.NotEqual(t, key1, key2)
  1506  			}
  1507  		})
  1508  	}
  1509  
  1510  }