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

     1  // Copyright 2017 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  	"strings"
    20  	"testing"
    21  	"time"
    22  
    23  	"agones.dev/agones/pkg"
    24  	"agones.dev/agones/pkg/apis"
    25  	"agones.dev/agones/pkg/apis/agones"
    26  	"agones.dev/agones/pkg/util/runtime"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/api/resource"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/util/validation"
    33  	"k8s.io/apimachinery/pkg/util/validation/field"
    34  )
    35  
    36  const (
    37  	ipFixture = "127.1.1.1"
    38  )
    39  
    40  func TestStatus(t *testing.T) {
    41  	testCases := map[string]struct {
    42  		hostPort      int32
    43  		containerPort int32
    44  		portPolicy    PortPolicy
    45  		expected      GameServerStatusPort
    46  	}{
    47  		"PortPolicy Dynamic, should use hostPort": {
    48  			hostPort:      7788,
    49  			containerPort: 7777,
    50  			portPolicy:    Dynamic,
    51  			expected:      GameServerStatusPort{Name: "test-name", Port: 7788},
    52  		},
    53  		"PortPolicy Static - should use hostPort": {
    54  			hostPort:      7788,
    55  			containerPort: 7777,
    56  			portPolicy:    Static,
    57  			expected:      GameServerStatusPort{Name: "test-name", Port: 7788},
    58  		},
    59  		"PortPolicy Passthrough - should use hostPort": {
    60  			hostPort:      7788,
    61  			containerPort: 7777,
    62  			portPolicy:    Passthrough,
    63  			expected:      GameServerStatusPort{Name: "test-name", Port: 7788},
    64  		},
    65  		"PortPolicy None - should use containerPort and ignore hostPort": {
    66  			hostPort:      7788,
    67  			containerPort: 7777,
    68  			portPolicy:    None,
    69  			expected:      GameServerStatusPort{Name: "test-name", Port: 7777},
    70  		},
    71  	}
    72  	runtime.FeatureTestMutex.Lock()
    73  	defer runtime.FeatureTestMutex.Unlock()
    74  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeaturePortPolicyNone)+"=true"))
    75  
    76  	for _, tc := range testCases {
    77  		name := "test-name"
    78  		p := GameServerPort{Name: name, HostPort: tc.hostPort, ContainerPort: tc.containerPort, PortPolicy: tc.portPolicy}
    79  
    80  		res := p.Status()
    81  		assert.Equal(t, tc.expected, res)
    82  
    83  	}
    84  }
    85  
    86  func TestIsBeingDeleted(t *testing.T) {
    87  	deletionTimestamp := metav1.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
    88  	testCases := []struct {
    89  		description string
    90  		gs          *GameServer
    91  		expected    bool
    92  	}{
    93  		{
    94  			description: "ready gs, is not being deleted",
    95  			gs: &GameServer{
    96  				ObjectMeta: metav1.ObjectMeta{
    97  					DeletionTimestamp: nil,
    98  				},
    99  				Status: GameServerStatus{State: GameServerStateReady},
   100  			},
   101  			expected: false,
   102  		},
   103  		{
   104  			description: "DeletionTimestamp is set, gs is being deleted",
   105  			gs: &GameServer{
   106  				ObjectMeta: metav1.ObjectMeta{
   107  					DeletionTimestamp: &deletionTimestamp,
   108  				},
   109  				Status: GameServerStatus{State: GameServerStateReady},
   110  			},
   111  			expected: true,
   112  		},
   113  		{
   114  			description: "gs status is GameServerStateShutdown, gs is being deleted",
   115  			gs: &GameServer{
   116  				ObjectMeta: metav1.ObjectMeta{
   117  					DeletionTimestamp: nil,
   118  				},
   119  				Status: GameServerStatus{State: GameServerStateShutdown},
   120  			},
   121  			expected: true,
   122  		},
   123  		{
   124  			description: "gs status is GameServerStateShutdown and DeletionTimestamp is set, gs is being deleted",
   125  			gs: &GameServer{
   126  				ObjectMeta: metav1.ObjectMeta{
   127  					DeletionTimestamp: &deletionTimestamp,
   128  				},
   129  				Status: GameServerStatus{State: GameServerStateShutdown},
   130  			},
   131  			expected: true,
   132  		},
   133  	}
   134  
   135  	for _, tc := range testCases {
   136  		t.Run(tc.description, func(t *testing.T) {
   137  			result := tc.gs.IsBeingDeleted()
   138  			assert.Equal(t, tc.expected, result)
   139  		})
   140  	}
   141  }
   142  
   143  func TestGameServerApplyDefaults(t *testing.T) {
   144  	t.Parallel()
   145  
   146  	ten := int64(10)
   147  
   148  	defaultGameServerAnd := func(f func(gss *GameServerSpec)) GameServer {
   149  		gs := GameServer{
   150  			Spec: GameServerSpec{
   151  				Ports: []GameServerPort{{ContainerPort: 999}},
   152  				Template: corev1.PodTemplateSpec{
   153  					Spec: corev1.PodSpec{Containers: []corev1.Container{
   154  						{Name: "testing", Image: "testing/image"},
   155  					}},
   156  				},
   157  			},
   158  		}
   159  		f(&gs.Spec)
   160  		return gs
   161  	}
   162  	type expected struct {
   163  		container           string
   164  		protocol            corev1.Protocol
   165  		state               GameServerState
   166  		policy              PortPolicy
   167  		portRange           string
   168  		health              Health
   169  		scheduling          apis.SchedulingStrategy
   170  		sdkServer           SdkServer
   171  		alphaPlayerCapacity *int64
   172  		counterSpec         map[string]CounterStatus
   173  		listSpec            map[string]ListStatus
   174  		evictionSafeSpec    EvictionSafe
   175  		evictionSafeStatus  EvictionSafe
   176  	}
   177  	wantDefaultAnd := func(f func(e *expected)) expected {
   178  		e := expected{
   179  			container:  "testing",
   180  			protocol:   "UDP",
   181  			portRange:  DefaultPortRange,
   182  			state:      GameServerStatePortAllocation,
   183  			policy:     Dynamic,
   184  			scheduling: apis.Packed,
   185  			health: Health{
   186  				Disabled:            false,
   187  				FailureThreshold:    3,
   188  				InitialDelaySeconds: 5,
   189  				PeriodSeconds:       5,
   190  			},
   191  			sdkServer: SdkServer{
   192  				LogLevel: SdkServerLogLevelInfo,
   193  				GRPCPort: 9357,
   194  				HTTPPort: 9358,
   195  			},
   196  			evictionSafeSpec:   EvictionSafeNever,
   197  			evictionSafeStatus: EvictionSafeNever,
   198  		}
   199  		f(&e)
   200  		return e
   201  	}
   202  	data := map[string]struct {
   203  		gameServer   GameServer
   204  		container    string
   205  		featureFlags string
   206  		expected     expected
   207  	}{
   208  		"set basic defaults on a very simple gameserver": {
   209  			gameServer: defaultGameServerAnd(func(_ *GameServerSpec) {}),
   210  			expected:   wantDefaultAnd(func(_ *expected) {}),
   211  		},
   212  		"PlayerTracking=true": {
   213  			featureFlags: string(runtime.FeaturePlayerTracking) + "=true",
   214  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   215  				gss.Players = &PlayersSpec{InitialCapacity: 10}
   216  			}),
   217  			expected: wantDefaultAnd(func(e *expected) {
   218  				e.alphaPlayerCapacity = &ten
   219  			}),
   220  		},
   221  		"CountsAndLists=true, Counters": {
   222  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   223  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   224  				gss.Counters = make(map[string]CounterStatus)
   225  				gss.Counters["games"] = CounterStatus{Count: 1, Capacity: 100}
   226  			}),
   227  			expected: wantDefaultAnd(func(e *expected) {
   228  				e.counterSpec = make(map[string]CounterStatus)
   229  				e.counterSpec["games"] = CounterStatus{Count: 1, Capacity: 100}
   230  			}),
   231  		},
   232  		"CountsAndLists=true, Lists": {
   233  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   234  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   235  				gss.Lists = make(map[string]ListStatus)
   236  				gss.Lists["players"] = ListStatus{Capacity: 100, Values: []string{"foo", "bar"}}
   237  			}),
   238  			expected: wantDefaultAnd(func(e *expected) {
   239  				e.listSpec = make(map[string]ListStatus)
   240  				e.listSpec["players"] = ListStatus{Capacity: 100, Values: []string{"foo", "bar"}}
   241  			}),
   242  		},
   243  		"defaults on passthrough": {
   244  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   245  				gss.Ports[0].PortPolicy = Passthrough
   246  			}),
   247  			expected: wantDefaultAnd(func(e *expected) {
   248  				e.policy = Passthrough
   249  			}),
   250  		},
   251  		"defaults are already set": {
   252  			gameServer: GameServer{
   253  				Spec: GameServerSpec{
   254  					Container: "testing2",
   255  					Ports: []GameServerPort{{
   256  						Protocol:   "TCP",
   257  						Range:      DefaultPortRange,
   258  						PortPolicy: Static,
   259  					}},
   260  					Health: Health{
   261  						Disabled:            false,
   262  						PeriodSeconds:       12,
   263  						InitialDelaySeconds: 11,
   264  						FailureThreshold:    10,
   265  					},
   266  					Template: corev1.PodTemplateSpec{
   267  						Spec: corev1.PodSpec{
   268  							Containers: []corev1.Container{
   269  								{Name: "testing", Image: "testing/image"},
   270  								{Name: "testing2", Image: "testing/image2"},
   271  							},
   272  						},
   273  					},
   274  					SdkServer: SdkServer{
   275  						LogLevel: SdkServerLogLevelInfo,
   276  						GRPCPort: 9357,
   277  						HTTPPort: 9358,
   278  					},
   279  				},
   280  				Status: GameServerStatus{State: "TestState"},
   281  			},
   282  			expected: wantDefaultAnd(func(e *expected) {
   283  				e.container = "testing2"
   284  				e.protocol = "TCP"
   285  				e.state = "TestState"
   286  				e.health = Health{
   287  					Disabled:            false,
   288  					FailureThreshold:    10,
   289  					InitialDelaySeconds: 11,
   290  					PeriodSeconds:       12,
   291  				}
   292  			}),
   293  		},
   294  		"set basic defaults on static gameserver": {
   295  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   296  				gss.Ports[0].PortPolicy = Static
   297  			}),
   298  			expected: wantDefaultAnd(func(e *expected) {
   299  				e.state = GameServerStateCreating
   300  				e.policy = Static
   301  			}),
   302  		},
   303  		"health is disabled": {
   304  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   305  				gss.Health = Health{Disabled: true}
   306  			}),
   307  			expected: wantDefaultAnd(func(e *expected) {
   308  				e.health = Health{Disabled: true}
   309  			}),
   310  		},
   311  		"convert from legacy single port to multiple": {
   312  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   313  				gss.Ports[0] = GameServerPort{
   314  					ContainerPort: 777,
   315  					HostPort:      777,
   316  					PortPolicy:    Static,
   317  					Protocol:      corev1.ProtocolTCP,
   318  				}
   319  			}),
   320  			expected: wantDefaultAnd(func(e *expected) {
   321  				e.protocol = "TCP"
   322  				e.state = GameServerStateCreating
   323  			}),
   324  		},
   325  		"set Debug logging level": {
   326  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   327  				gss.SdkServer = SdkServer{LogLevel: SdkServerLogLevelDebug}
   328  			}),
   329  			expected: wantDefaultAnd(func(e *expected) {
   330  				e.sdkServer.LogLevel = SdkServerLogLevelDebug
   331  			}),
   332  		},
   333  		"set gRPC and HTTP ports on SDK Server": {
   334  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   335  				gss.SdkServer = SdkServer{
   336  					LogLevel: SdkServerLogLevelError,
   337  					GRPCPort: 19357,
   338  					HTTPPort: 19358,
   339  				}
   340  			}),
   341  			expected: wantDefaultAnd(func(e *expected) {
   342  				e.sdkServer = SdkServer{
   343  					LogLevel: SdkServerLogLevelError,
   344  					GRPCPort: 19357,
   345  					HTTPPort: 19358,
   346  				}
   347  			}),
   348  		},
   349  		"defaults are eviction.safe: Never": {
   350  			gameServer: defaultGameServerAnd(func(_ *GameServerSpec) {}),
   351  			expected: wantDefaultAnd(func(e *expected) {
   352  				e.evictionSafeSpec = EvictionSafeNever
   353  				e.evictionSafeStatus = EvictionSafeNever
   354  			}),
   355  		},
   356  		"eviction.safe: Always": {
   357  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   358  				gss.Eviction = &Eviction{Safe: EvictionSafeAlways}
   359  			}),
   360  			expected: wantDefaultAnd(func(e *expected) {
   361  				e.evictionSafeSpec = EvictionSafeAlways
   362  				e.evictionSafeStatus = EvictionSafeAlways
   363  			}),
   364  		},
   365  		"eviction.safe: OnUpgrade": {
   366  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   367  				gss.Eviction = &Eviction{Safe: EvictionSafeOnUpgrade}
   368  			}),
   369  			expected: wantDefaultAnd(func(e *expected) {
   370  				e.evictionSafeSpec = EvictionSafeOnUpgrade
   371  				e.evictionSafeStatus = EvictionSafeOnUpgrade
   372  			}),
   373  		},
   374  		"eviction.safe: Never": {
   375  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   376  				gss.Eviction = &Eviction{Safe: EvictionSafeNever}
   377  			}),
   378  			expected: wantDefaultAnd(func(e *expected) {
   379  				e.evictionSafeSpec = EvictionSafeNever
   380  				e.evictionSafeStatus = EvictionSafeNever
   381  			}),
   382  		},
   383  		"eviction.safe: Always inferred from safe-to-evict=true": {
   384  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   385  				gss.Template.ObjectMeta.Annotations = map[string]string{PodSafeToEvictAnnotation: "true"}
   386  			}),
   387  			expected: wantDefaultAnd(func(e *expected) {
   388  				e.evictionSafeSpec = EvictionSafeNever
   389  				e.evictionSafeStatus = EvictionSafeAlways
   390  			}),
   391  		},
   392  		"Nothing inferred from safe-to-evict=false": {
   393  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   394  				gss.Template.ObjectMeta.Annotations = map[string]string{PodSafeToEvictAnnotation: "false"}
   395  			}),
   396  			expected: wantDefaultAnd(func(e *expected) {
   397  				e.evictionSafeSpec = EvictionSafeNever
   398  				e.evictionSafeStatus = EvictionSafeNever
   399  			}),
   400  		},
   401  		"safe-to-evict=false AND eviction.safe: Always => eviction.safe: Always": {
   402  			gameServer: defaultGameServerAnd(func(gss *GameServerSpec) {
   403  				gss.Eviction = &Eviction{Safe: EvictionSafeAlways}
   404  				gss.Template.ObjectMeta.Annotations = map[string]string{PodSafeToEvictAnnotation: "false"}
   405  			}),
   406  			expected: wantDefaultAnd(func(e *expected) {
   407  				e.evictionSafeSpec = EvictionSafeAlways
   408  				e.evictionSafeStatus = EvictionSafeAlways
   409  			}),
   410  		},
   411  	}
   412  
   413  	runtime.FeatureTestMutex.Lock()
   414  	defer runtime.FeatureTestMutex.Unlock()
   415  
   416  	for name, test := range data {
   417  		t.Run(name, func(t *testing.T) {
   418  			err := runtime.ParseFeatures(test.featureFlags)
   419  			assert.NoError(t, err)
   420  
   421  			test.gameServer.ApplyDefaults()
   422  
   423  			assert.Equal(t, pkg.Version, test.gameServer.Annotations[VersionAnnotation])
   424  
   425  			spec := test.gameServer.Spec
   426  			assert.Contains(t, test.gameServer.ObjectMeta.Finalizers, FinalizerName)
   427  			assert.Equal(t, test.expected.container, spec.Container)
   428  			assert.Equal(t, test.expected.protocol, spec.Ports[0].Protocol)
   429  			assert.Equal(t, test.expected.portRange, spec.Ports[0].Range)
   430  			assert.Equal(t, test.expected.state, test.gameServer.Status.State)
   431  			assert.Equal(t, test.expected.scheduling, test.gameServer.Spec.Scheduling)
   432  			assert.Equal(t, test.expected.health, test.gameServer.Spec.Health)
   433  			assert.Equal(t, test.expected.sdkServer, test.gameServer.Spec.SdkServer)
   434  			if test.expected.alphaPlayerCapacity != nil {
   435  				assert.Equal(t, *test.expected.alphaPlayerCapacity, test.gameServer.Status.Players.Capacity)
   436  			} else {
   437  				assert.Nil(t, test.gameServer.Spec.Players)
   438  				assert.Nil(t, test.gameServer.Status.Players)
   439  			}
   440  			if len(test.expected.evictionSafeSpec) > 0 {
   441  				assert.Equal(t, test.expected.evictionSafeSpec, spec.Eviction.Safe)
   442  			} else {
   443  				assert.Nil(t, spec.Eviction)
   444  			}
   445  			if len(test.expected.evictionSafeStatus) > 0 {
   446  				assert.Equal(t, test.expected.evictionSafeStatus, test.gameServer.Status.Eviction.Safe)
   447  			} else {
   448  				assert.Nil(t, test.gameServer.Status.Eviction)
   449  			}
   450  			if test.expected.counterSpec != nil {
   451  				assert.Equal(t, test.expected.counterSpec, test.gameServer.Status.Counters)
   452  			} else {
   453  				assert.Nil(t, test.gameServer.Spec.Counters)
   454  				assert.Nil(t, test.gameServer.Status.Counters)
   455  			}
   456  			if test.expected.listSpec != nil {
   457  				assert.Equal(t, test.expected.listSpec, test.gameServer.Status.Lists)
   458  			} else {
   459  				assert.Nil(t, test.gameServer.Spec.Lists)
   460  				assert.Nil(t, test.gameServer.Status.Lists)
   461  			}
   462  		})
   463  	}
   464  }
   465  
   466  // nolint:dupl
   467  func TestGameServerValidate(t *testing.T) {
   468  	t.Parallel()
   469  
   470  	longNameLen64 := strings.Repeat("f", validation.LabelValueMaxLength+1)
   471  
   472  	testCases := []struct {
   473  		description   string
   474  		gs            GameServer
   475  		applyDefaults bool
   476  		want          field.ErrorList
   477  	}{
   478  		{
   479  			description: "Valid game server",
   480  			gs: GameServer{
   481  				Spec: GameServerSpec{
   482  					Template: corev1.PodTemplateSpec{
   483  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
   484  					},
   485  				},
   486  			},
   487  			applyDefaults: true,
   488  		},
   489  		{
   490  			description: "Invalid gs: container, containerPort, hostPort",
   491  			gs: GameServer{
   492  				Spec: GameServerSpec{
   493  					Container: "",
   494  					Ports: []GameServerPort{{
   495  						Name:       "main",
   496  						HostPort:   5001,
   497  						PortPolicy: Dynamic,
   498  					}, {
   499  						Name:          "sidecar",
   500  						HostPort:      5002,
   501  						PortPolicy:    Static,
   502  						ContainerPort: 5002,
   503  					}},
   504  					Template: corev1.PodTemplateSpec{
   505  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   506  							{Name: "testing", Image: "testing/image"},
   507  							{Name: "anothertest", Image: "testing/image"},
   508  						}},
   509  					},
   510  				},
   511  			},
   512  			applyDefaults: false,
   513  			want: field.ErrorList{
   514  				field.Required(field.NewPath("spec", "container"), "Container is required when using multiple containers in the pod template"),
   515  				field.Invalid(field.NewPath("spec", "container"), "", "Could not find a container named "),
   516  				field.Required(field.NewPath("spec", "ports").Index(0).Child("containerPort"), "ContainerPort must be defined for Dynamic and Static PortPolicies"),
   517  				field.Forbidden(field.NewPath("spec", "ports").Index(0).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"),
   518  			},
   519  		},
   520  		{
   521  			description: "DevAddressAnnotation: Invalid IP, no host port",
   522  			gs: GameServer{
   523  				ObjectMeta: metav1.ObjectMeta{
   524  					Name:        "dev-game",
   525  					Namespace:   "default",
   526  					Annotations: map[string]string{DevAddressAnnotation: "invalid-ip"},
   527  				},
   528  				Spec: GameServerSpec{
   529  					Ports: []GameServerPort{{Name: "main", ContainerPort: 7777, PortPolicy: Static}},
   530  				},
   531  			},
   532  			applyDefaults: false,
   533  			want: field.ErrorList{
   534  				field.Invalid(field.NewPath("metadata").Child("annotations", "agones.dev/dev-address"), "invalid-ip", "must be a valid IP address"),
   535  				field.Required(field.NewPath("spec").Child("ports").Index(0).Child("hostPort"), "agones.dev/dev-address"),
   536  			},
   537  		},
   538  		{
   539  			description: "Long gs name",
   540  			gs: GameServer{
   541  				ObjectMeta: metav1.ObjectMeta{
   542  					Name: longNameLen64,
   543  				},
   544  				TypeMeta: metav1.TypeMeta{
   545  					Kind: "test-kind",
   546  				},
   547  				Spec: GameServerSpec{
   548  					Container: "my_image",
   549  					Template: corev1.PodTemplateSpec{
   550  						Spec: corev1.PodSpec{
   551  							Containers: []corev1.Container{
   552  								{Name: "my_image", Image: "foo/my_image"},
   553  							},
   554  						},
   555  					},
   556  				},
   557  			},
   558  			applyDefaults: false,
   559  			want: field.ErrorList{
   560  				field.TooLongMaxLength(field.NewPath("metadata", "name"), longNameLen64, 63),
   561  			},
   562  		},
   563  		{
   564  			description: "Long gs GenerateName is not validated on agones side",
   565  			gs: GameServer{
   566  				ObjectMeta: metav1.ObjectMeta{
   567  					GenerateName: longNameLen64,
   568  				},
   569  				TypeMeta: metav1.TypeMeta{
   570  					Kind: "test-kind",
   571  				},
   572  				Spec: GameServerSpec{
   573  					Container: "my_image",
   574  					Template: corev1.PodTemplateSpec{
   575  						Spec: corev1.PodSpec{
   576  							Containers: []corev1.Container{
   577  								{Name: "my_image", Image: "foo/my_image"},
   578  							},
   579  						},
   580  					},
   581  				},
   582  			},
   583  			applyDefaults: false,
   584  		},
   585  		{
   586  			description: "Long label key is invalid",
   587  			gs: GameServer{
   588  				ObjectMeta: metav1.ObjectMeta{
   589  					GenerateName: longNameLen64,
   590  				},
   591  				TypeMeta: metav1.TypeMeta{
   592  					Kind: "test-kind",
   593  				},
   594  				Spec: GameServerSpec{
   595  					Container: "my_image",
   596  					Template: corev1.PodTemplateSpec{
   597  						Spec: corev1.PodSpec{
   598  							Containers: []corev1.Container{
   599  								{Name: "my_image", Image: "foo/my_image"},
   600  							},
   601  						},
   602  						ObjectMeta: metav1.ObjectMeta{
   603  							Labels: map[string]string{longNameLen64: ""},
   604  						},
   605  					},
   606  				},
   607  			},
   608  			applyDefaults: false,
   609  			want: field.ErrorList{
   610  				&field.Error{
   611  					Type:     field.ErrorTypeInvalid,
   612  					Field:    field.NewPath("spec", "template", "metadata", "labels").String(),
   613  					BadValue: longNameLen64,
   614  					Detail:   "name part must be no more than 63 characters",
   615  					Origin:   "labelKey",
   616  				},
   617  			},
   618  		},
   619  		{
   620  			description: "Long label value is invalid",
   621  			gs: GameServer{
   622  				ObjectMeta: metav1.ObjectMeta{
   623  					GenerateName: "ok-name",
   624  				},
   625  				TypeMeta: metav1.TypeMeta{
   626  					Kind: "test-kind",
   627  				},
   628  				Spec: GameServerSpec{
   629  					Container: "my_image",
   630  					Template: corev1.PodTemplateSpec{
   631  						Spec: corev1.PodSpec{
   632  							Containers: []corev1.Container{
   633  								{Name: "my_image", Image: "foo/my_image"},
   634  							},
   635  						},
   636  						ObjectMeta: metav1.ObjectMeta{
   637  							Labels: map[string]string{"agones.dev/longValueKey": longNameLen64},
   638  						},
   639  					},
   640  				},
   641  			},
   642  			applyDefaults: false,
   643  			want: field.ErrorList{
   644  				field.Invalid(field.NewPath("spec", "template", "metadata", "labels"), longNameLen64, "must be no more than 63 characters"),
   645  			},
   646  		},
   647  		{
   648  			description: "Long annotation key is invalid",
   649  			gs: GameServer{
   650  				ObjectMeta: metav1.ObjectMeta{
   651  					GenerateName: "ok-name",
   652  				},
   653  				TypeMeta: metav1.TypeMeta{
   654  					Kind: "test-kind",
   655  				},
   656  				Spec: GameServerSpec{
   657  					Container: "my_image",
   658  					Template: corev1.PodTemplateSpec{
   659  						Spec: corev1.PodSpec{
   660  							Containers: []corev1.Container{
   661  								{Name: "my_image", Image: "foo/my_image"},
   662  							},
   663  						},
   664  						ObjectMeta: metav1.ObjectMeta{
   665  							Annotations: map[string]string{longNameLen64: longNameLen64},
   666  						},
   667  					},
   668  				},
   669  			},
   670  			applyDefaults: false,
   671  			want: field.ErrorList{
   672  				field.Invalid(field.NewPath("spec", "template", "metadata", "annotations"), longNameLen64, "name part must be no more than 63 characters"),
   673  			},
   674  		},
   675  		{
   676  			description: "Invalid character in annotation key",
   677  			gs: GameServer{
   678  				ObjectMeta: metav1.ObjectMeta{
   679  					GenerateName: "ok-name",
   680  				},
   681  				TypeMeta: metav1.TypeMeta{
   682  					Kind: "test-kind",
   683  				},
   684  				Spec: GameServerSpec{
   685  					Container: "my_image",
   686  					Template: corev1.PodTemplateSpec{
   687  						Spec: corev1.PodSpec{
   688  							Containers: []corev1.Container{
   689  								{Name: "my_image", Image: "foo/my_image"},
   690  							},
   691  						},
   692  						ObjectMeta: metav1.ObjectMeta{
   693  							Annotations: map[string]string{"agones.dev/short±Name": longNameLen64},
   694  						},
   695  					},
   696  				},
   697  			},
   698  			applyDefaults: false,
   699  			want: field.ErrorList{
   700  				field.Invalid(field.NewPath("spec", "template", "metadata", "annotations"), "agones.dev/short±Name", "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]')"),
   701  			},
   702  		},
   703  		{
   704  			description: "Valid annotation key",
   705  			gs: GameServer{
   706  				ObjectMeta: metav1.ObjectMeta{
   707  					GenerateName: "ok-name",
   708  				},
   709  				TypeMeta: metav1.TypeMeta{
   710  					Kind: "test-kind",
   711  				},
   712  				Spec: GameServerSpec{
   713  					Container: "my_image",
   714  					Template: corev1.PodTemplateSpec{
   715  						Spec: corev1.PodSpec{
   716  							Containers: []corev1.Container{
   717  								{Name: "my_image", Image: "foo/my_image"},
   718  							},
   719  						},
   720  						ObjectMeta: metav1.ObjectMeta{
   721  							Annotations: map[string]string{"agones.dev/shortName": longNameLen64},
   722  						},
   723  					},
   724  				},
   725  			},
   726  			applyDefaults: false,
   727  		},
   728  		{
   729  			description: "Check ContainerPort and HostPort with different policies",
   730  			gs: GameServer{
   731  				ObjectMeta: metav1.ObjectMeta{
   732  					GenerateName: "ok-name",
   733  				},
   734  				TypeMeta: metav1.TypeMeta{
   735  					Kind: "test-kind",
   736  				},
   737  				Spec: GameServerSpec{
   738  					Ports: []GameServerPort{
   739  						{Name: "one", PortPolicy: Passthrough, ContainerPort: 1294},
   740  						{PortPolicy: Passthrough, Name: "two", HostPort: 7890},
   741  						{PortPolicy: Dynamic, Name: "three", HostPort: 7890, ContainerPort: 1294},
   742  					},
   743  					Container: "my_image",
   744  					Template: corev1.PodTemplateSpec{
   745  						Spec: corev1.PodSpec{
   746  							Containers: []corev1.Container{
   747  								{Name: "my_image", Image: "foo/my_image"},
   748  							},
   749  						},
   750  					},
   751  				},
   752  			},
   753  			applyDefaults: true,
   754  			want: field.ErrorList{
   755  				field.Required(field.NewPath("spec", "ports").Index(0).Child("containerPort"), "ContainerPort cannot be specified with Passthrough PortPolicy"),
   756  				field.Forbidden(field.NewPath("spec", "ports").Index(1).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"),
   757  				field.Forbidden(field.NewPath("spec", "ports").Index(2).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"),
   758  			},
   759  		},
   760  		{
   761  			description: "PortPolicy must be Static with HostPort specified",
   762  			gs: GameServer{
   763  				ObjectMeta: metav1.ObjectMeta{
   764  					Name:        "dev-game",
   765  					Namespace:   "default",
   766  					Annotations: map[string]string{DevAddressAnnotation: ipFixture},
   767  				},
   768  				Spec: GameServerSpec{
   769  					Ports: []GameServerPort{
   770  						{PortPolicy: Passthrough, Name: "main", HostPort: 7890, ContainerPort: 7777},
   771  					},
   772  					Container: "my_image",
   773  					Template: corev1.PodTemplateSpec{
   774  						Spec: corev1.PodSpec{
   775  							Containers: []corev1.Container{
   776  								{Name: "my_image", Image: "foo/my_image"},
   777  							},
   778  						},
   779  					},
   780  				},
   781  			},
   782  			applyDefaults: true,
   783  			want: field.ErrorList{
   784  				field.Required(
   785  					field.NewPath("spec", "ports").Index(0).Child("portPolicy"),
   786  					"PortPolicy must be Static"),
   787  			},
   788  		},
   789  		{
   790  			description: "ContainerPort is less than zero",
   791  			gs: GameServer{
   792  				ObjectMeta: metav1.ObjectMeta{
   793  					Name:      "dev-game",
   794  					Namespace: "default",
   795  				},
   796  				Spec: GameServerSpec{
   797  					Ports: []GameServerPort{{
   798  						Name:          "main",
   799  						ContainerPort: -4,
   800  						PortPolicy:    Dynamic,
   801  					}},
   802  					Container: "testing",
   803  					Template: corev1.PodTemplateSpec{
   804  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   805  							{Name: "testing", Image: "testing/image"},
   806  						}},
   807  					},
   808  				},
   809  			},
   810  			applyDefaults: false,
   811  			want: field.ErrorList{
   812  				field.Required(
   813  					field.NewPath("spec", "ports").Index(0).Child("containerPort"),
   814  					"ContainerPort must be defined for Dynamic and Static PortPolicies",
   815  				),
   816  			},
   817  		},
   818  		{
   819  			description: "CPU Request > Limit",
   820  			gs: GameServer{
   821  				ObjectMeta: metav1.ObjectMeta{
   822  					Name:      "dev-game",
   823  					Namespace: "default",
   824  				},
   825  				Spec: GameServerSpec{
   826  					Ports: []GameServerPort{{
   827  						Name:          "main",
   828  						ContainerPort: 7777,
   829  						PortPolicy:    Dynamic,
   830  					}},
   831  					Container: "testing",
   832  					Template: corev1.PodTemplateSpec{
   833  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   834  							{
   835  								Name:  "testing",
   836  								Image: "testing/image",
   837  								Resources: corev1.ResourceRequirements{
   838  									Requests: corev1.ResourceList{
   839  										corev1.ResourceCPU:    resource.MustParse("50m"),
   840  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   841  									},
   842  									Limits: corev1.ResourceList{
   843  										corev1.ResourceCPU:    resource.MustParse("30m"),
   844  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   845  									},
   846  								},
   847  							},
   848  						}},
   849  					},
   850  				},
   851  			},
   852  			applyDefaults: false,
   853  			want: field.ErrorList{
   854  				field.Invalid(
   855  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
   856  					"50m",
   857  					"must be less than or equal to cpu limit of 30m",
   858  				),
   859  			},
   860  		},
   861  		{
   862  			description: "CPU negative request",
   863  			gs: GameServer{
   864  				ObjectMeta: metav1.ObjectMeta{
   865  					Name:      "dev-game",
   866  					Namespace: "default",
   867  				},
   868  				Spec: GameServerSpec{
   869  					Ports: []GameServerPort{{
   870  						Name:          "main",
   871  						ContainerPort: 7777,
   872  						PortPolicy:    Dynamic,
   873  					}},
   874  					Container: "testing",
   875  					Template: corev1.PodTemplateSpec{
   876  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   877  							{
   878  								Name:  "testing",
   879  								Image: "testing/image",
   880  								Resources: corev1.ResourceRequirements{
   881  									Requests: corev1.ResourceList{
   882  										corev1.ResourceCPU:    resource.MustParse("-30m"),
   883  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   884  									},
   885  									Limits: corev1.ResourceList{
   886  										corev1.ResourceCPU:    resource.MustParse("30m"),
   887  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   888  									},
   889  								},
   890  							},
   891  						}},
   892  					},
   893  				},
   894  			},
   895  			applyDefaults: false,
   896  			want: field.ErrorList{
   897  				field.Invalid(
   898  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("cpu"),
   899  					"-30m",
   900  					"must be greater than or equal to 0",
   901  				),
   902  			},
   903  		},
   904  		{
   905  			description: "CPU negative limit",
   906  			gs: GameServer{
   907  				ObjectMeta: metav1.ObjectMeta{
   908  					Name:      "dev-game",
   909  					Namespace: "default",
   910  				},
   911  				Spec: GameServerSpec{
   912  					Ports: []GameServerPort{{
   913  						Name:          "main",
   914  						ContainerPort: 7777,
   915  						PortPolicy:    Dynamic,
   916  					}},
   917  					Container: "testing",
   918  					Template: corev1.PodTemplateSpec{
   919  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   920  							{
   921  								Name:  "testing",
   922  								Image: "testing/image",
   923  								Resources: corev1.ResourceRequirements{
   924  									Requests: corev1.ResourceList{
   925  										corev1.ResourceCPU:    resource.MustParse("30m"),
   926  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   927  									},
   928  									Limits: corev1.ResourceList{
   929  										corev1.ResourceCPU:    resource.MustParse("-30m"),
   930  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   931  									},
   932  								},
   933  							},
   934  						}},
   935  					},
   936  				},
   937  			},
   938  			applyDefaults: false,
   939  			want: field.ErrorList{
   940  				field.Invalid(
   941  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("cpu"),
   942  					"-30m",
   943  					"must be greater than or equal to 0",
   944  				),
   945  				field.Invalid(
   946  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
   947  					"30m",
   948  					"must be less than or equal to cpu limit of -30m",
   949  				),
   950  			},
   951  		},
   952  		{
   953  			description: "Memory Request > Limit",
   954  			gs: GameServer{
   955  				ObjectMeta: metav1.ObjectMeta{
   956  					Name:      "dev-game",
   957  					Namespace: "default",
   958  				},
   959  				Spec: GameServerSpec{
   960  					Ports: []GameServerPort{{
   961  						Name:          "main",
   962  						ContainerPort: 7777,
   963  						PortPolicy:    Dynamic,
   964  					}},
   965  					Container: "testing",
   966  					Template: corev1.PodTemplateSpec{
   967  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   968  							{
   969  								Name:  "testing",
   970  								Image: "testing/image",
   971  								Resources: corev1.ResourceRequirements{
   972  									Requests: corev1.ResourceList{
   973  										corev1.ResourceCPU:    resource.MustParse("30m"),
   974  										corev1.ResourceMemory: resource.MustParse("55Mi"),
   975  									},
   976  									Limits: corev1.ResourceList{
   977  										corev1.ResourceCPU:    resource.MustParse("30m"),
   978  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   979  									},
   980  								},
   981  							},
   982  						}},
   983  					},
   984  				},
   985  			},
   986  			applyDefaults: false,
   987  			want: field.ErrorList{
   988  				field.Invalid(
   989  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
   990  					"55Mi",
   991  					"must be less than or equal to memory limit of 32Mi",
   992  				),
   993  			},
   994  		},
   995  		{
   996  			description: "Memory negative request",
   997  			gs: GameServer{
   998  				ObjectMeta: metav1.ObjectMeta{
   999  					Name:      "dev-game",
  1000  					Namespace: "default",
  1001  				},
  1002  				Spec: GameServerSpec{
  1003  					Ports: []GameServerPort{{
  1004  						Name:          "main",
  1005  						ContainerPort: 7777,
  1006  						PortPolicy:    Dynamic,
  1007  					}},
  1008  					Container: "testing",
  1009  					Template: corev1.PodTemplateSpec{
  1010  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1011  							{
  1012  								Name:  "testing",
  1013  								Image: "testing/image",
  1014  								Resources: corev1.ResourceRequirements{
  1015  									Requests: corev1.ResourceList{
  1016  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1017  										corev1.ResourceMemory: resource.MustParse("-32Mi"),
  1018  									},
  1019  									Limits: corev1.ResourceList{
  1020  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1021  										corev1.ResourceMemory: resource.MustParse("32Mi"),
  1022  									},
  1023  								},
  1024  							},
  1025  						}},
  1026  					},
  1027  				},
  1028  			},
  1029  			applyDefaults: false,
  1030  			want: field.ErrorList{
  1031  				field.Invalid(
  1032  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("memory"),
  1033  					"-32Mi",
  1034  					"must be greater than or equal to 0",
  1035  				),
  1036  			},
  1037  		},
  1038  		{
  1039  			description: "Memory negative limit",
  1040  			gs: GameServer{
  1041  				ObjectMeta: metav1.ObjectMeta{
  1042  					Name:      "dev-game",
  1043  					Namespace: "default",
  1044  				},
  1045  				Spec: GameServerSpec{
  1046  					Ports: []GameServerPort{{
  1047  						Name:          "main",
  1048  						ContainerPort: 7777,
  1049  						PortPolicy:    Dynamic,
  1050  					}},
  1051  					Container: "testing",
  1052  					Template: corev1.PodTemplateSpec{
  1053  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1054  							{
  1055  								Name:  "testing",
  1056  								Image: "testing/image",
  1057  								Resources: corev1.ResourceRequirements{
  1058  									Requests: corev1.ResourceList{
  1059  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1060  										corev1.ResourceMemory: resource.MustParse("32Mi"),
  1061  									},
  1062  									Limits: corev1.ResourceList{
  1063  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1064  										corev1.ResourceMemory: resource.MustParse("-32Mi"),
  1065  									},
  1066  								},
  1067  							},
  1068  						}},
  1069  					},
  1070  				},
  1071  			},
  1072  			applyDefaults: false,
  1073  			want: field.ErrorList{
  1074  				field.Invalid(
  1075  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("memory"),
  1076  					"-32Mi",
  1077  					"must be greater than or equal to 0",
  1078  				),
  1079  				field.Invalid(
  1080  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
  1081  					"32Mi",
  1082  					"must be less than or equal to memory limit of -32Mi",
  1083  				),
  1084  			},
  1085  		},
  1086  	}
  1087  
  1088  	for _, tc := range testCases {
  1089  		t.Run(tc.description, func(t *testing.T) {
  1090  			if tc.applyDefaults {
  1091  				tc.gs.ApplyDefaults()
  1092  			}
  1093  
  1094  			errs := tc.gs.Validate(fakeAPIHooks{})
  1095  			assert.ElementsMatch(t, tc.want, errs, "ErrorList check")
  1096  		})
  1097  	}
  1098  }
  1099  
  1100  func TestGameServerValidateFeatures(t *testing.T) {
  1101  	t.Parallel()
  1102  	runtime.FeatureTestMutex.Lock()
  1103  	defer runtime.FeatureTestMutex.Unlock()
  1104  
  1105  	portContainerName := "another-container"
  1106  
  1107  	testCases := []struct {
  1108  		description string
  1109  		feature     string
  1110  		gs          GameServer
  1111  		want        field.ErrorList
  1112  	}{
  1113  		{
  1114  			description: "Invalid container name, container was not found",
  1115  			gs: GameServer{
  1116  				ObjectMeta: metav1.ObjectMeta{
  1117  					Name:      "dev-game",
  1118  					Namespace: "default",
  1119  				},
  1120  				Spec: GameServerSpec{
  1121  					Ports: []GameServerPort{
  1122  						{
  1123  							Name:          "main",
  1124  							ContainerPort: 7777,
  1125  							PortPolicy:    Dynamic,
  1126  							Container:     &portContainerName,
  1127  						},
  1128  					},
  1129  					Container: "testing",
  1130  					Template: corev1.PodTemplateSpec{
  1131  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1132  							{Name: "testing", Image: "testing/image"},
  1133  						}},
  1134  					},
  1135  				},
  1136  			},
  1137  			want: field.ErrorList{
  1138  				field.Invalid(
  1139  					field.NewPath("spec", "ports").Index(0).Child("container"),
  1140  					"another-container",
  1141  					"Container must be empty or the name of a container in the pod template",
  1142  				),
  1143  			},
  1144  		},
  1145  		{
  1146  			description: "Multiple container ports, OK scenario",
  1147  			gs: GameServer{
  1148  				ObjectMeta: metav1.ObjectMeta{
  1149  					Name:      "dev-game",
  1150  					Namespace: "default",
  1151  				},
  1152  				Spec: GameServerSpec{
  1153  					Ports: []GameServerPort{
  1154  						{
  1155  							Name:          "main",
  1156  							ContainerPort: 7777,
  1157  							PortPolicy:    Dynamic,
  1158  						},
  1159  					},
  1160  					Container: "testing",
  1161  					Template: corev1.PodTemplateSpec{
  1162  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1163  							{Name: "testing", Image: "testing/image"},
  1164  						}},
  1165  					},
  1166  				},
  1167  			},
  1168  		},
  1169  		{
  1170  			description: "PlayerTracking is disabled, Players field specified",
  1171  			feature:     fmt.Sprintf("%s=false", runtime.FeaturePlayerTracking),
  1172  			gs: GameServer{
  1173  				Spec: GameServerSpec{
  1174  					Container: "testing",
  1175  					Players:   &PlayersSpec{InitialCapacity: 10},
  1176  					Template: corev1.PodTemplateSpec{
  1177  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1178  					},
  1179  				},
  1180  			},
  1181  			want: field.ErrorList{
  1182  				field.Forbidden(
  1183  					field.NewPath("spec", "players"),
  1184  					"Value cannot be set unless feature flag PlayerTracking is enabled",
  1185  				),
  1186  			},
  1187  		},
  1188  		{
  1189  			description: "PlayerTracking is enabled, Players field specified",
  1190  			feature:     fmt.Sprintf("%s=true", runtime.FeaturePlayerTracking),
  1191  			gs: GameServer{
  1192  				Spec: GameServerSpec{
  1193  					Container: "testing",
  1194  					Players:   &PlayersSpec{InitialCapacity: 10},
  1195  					Template: corev1.PodTemplateSpec{
  1196  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1197  					},
  1198  				},
  1199  			},
  1200  		},
  1201  		{
  1202  			description: "CountsAndLists is disabled, Counters field specified",
  1203  			feature:     fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists),
  1204  			gs: GameServer{
  1205  				Spec: GameServerSpec{
  1206  					Container: "testing",
  1207  					Counters:  map[string]CounterStatus{},
  1208  					Template: corev1.PodTemplateSpec{
  1209  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1210  					},
  1211  				},
  1212  			},
  1213  			want: field.ErrorList{
  1214  				field.Forbidden(
  1215  					field.NewPath("spec", "counters"),
  1216  					"Value cannot be set unless feature flag CountsAndLists is enabled",
  1217  				),
  1218  			},
  1219  		},
  1220  		{
  1221  			description: "CountsAndLists is disabled, Lists field specified",
  1222  			feature:     fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists),
  1223  			gs: GameServer{
  1224  				Spec: GameServerSpec{
  1225  					Container: "testing",
  1226  					Lists:     map[string]ListStatus{},
  1227  					Template: corev1.PodTemplateSpec{
  1228  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1229  					},
  1230  				},
  1231  			},
  1232  			want: field.ErrorList{
  1233  				field.Forbidden(
  1234  					field.NewPath("spec", "lists"),
  1235  					"Value cannot be set unless feature flag CountsAndLists is enabled",
  1236  				),
  1237  			},
  1238  		},
  1239  		{
  1240  			description: "CountsAndLists is enabled, Counters field specified",
  1241  			feature:     fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists),
  1242  			gs: GameServer{
  1243  				Spec: GameServerSpec{
  1244  					Container: "testing",
  1245  					Counters:  map[string]CounterStatus{},
  1246  					Template: corev1.PodTemplateSpec{
  1247  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1248  					},
  1249  				},
  1250  			},
  1251  		},
  1252  		{
  1253  			description: "CountsAndLists is enabled, Lists field specified",
  1254  			feature:     fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists),
  1255  			gs: GameServer{
  1256  				Spec: GameServerSpec{
  1257  					Container: "testing",
  1258  					Lists:     map[string]ListStatus{},
  1259  					Template: corev1.PodTemplateSpec{
  1260  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1261  					},
  1262  				},
  1263  			},
  1264  		},
  1265  		{
  1266  			description: "PortPolicyNone is disabled, PortPolicy field set to None",
  1267  			feature:     fmt.Sprintf("%s=false", runtime.FeaturePortPolicyNone),
  1268  			gs: GameServer{
  1269  				Spec: GameServerSpec{
  1270  					Ports: []GameServerPort{
  1271  						{
  1272  							Name:          "main",
  1273  							ContainerPort: 7777,
  1274  							PortPolicy:    None,
  1275  						},
  1276  					},
  1277  					Container: "testing",
  1278  					Lists:     map[string]ListStatus{},
  1279  					Template: corev1.PodTemplateSpec{
  1280  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1281  					},
  1282  				},
  1283  			},
  1284  			want: field.ErrorList{
  1285  				field.Forbidden(
  1286  					field.NewPath("spec.ports[0].portPolicy"),
  1287  					"Value cannot be set to None unless feature flag PortPolicyNone is enabled",
  1288  				),
  1289  			},
  1290  		},
  1291  		{
  1292  			description: "PortPolicyNone is enabled, PortPolicy field set to None",
  1293  			feature:     fmt.Sprintf("%s=true", runtime.FeaturePortPolicyNone),
  1294  			gs: GameServer{
  1295  				Spec: GameServerSpec{
  1296  					Ports: []GameServerPort{
  1297  						{
  1298  							Name:          "main",
  1299  							ContainerPort: 7777,
  1300  							PortPolicy:    None,
  1301  						},
  1302  					},
  1303  					Container: "testing",
  1304  					Lists:     map[string]ListStatus{},
  1305  					Template: corev1.PodTemplateSpec{
  1306  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1307  					},
  1308  				},
  1309  			},
  1310  		},
  1311  	}
  1312  
  1313  	for _, tc := range testCases {
  1314  		t.Run(tc.description, func(t *testing.T) {
  1315  			err := runtime.ParseFeatures(tc.feature)
  1316  			assert.NoError(t, err)
  1317  
  1318  			errs := tc.gs.Validate(fakeAPIHooks{})
  1319  			assert.ElementsMatch(t, tc.want, errs, "ErrorList check")
  1320  		})
  1321  	}
  1322  }
  1323  
  1324  func TestGameServerPodNoErrors(t *testing.T) {
  1325  	t.Parallel()
  1326  	fixture := defaultGameServer()
  1327  	fixture.ApplyDefaults()
  1328  
  1329  	pod, err := fixture.Pod(fakeAPIHooks{})
  1330  	assert.Nil(t, err, "Pod should not return an error")
  1331  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1332  	assert.Equal(t, fixture.ObjectMeta.Name, pod.Spec.Hostname)
  1333  	assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace)
  1334  	assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"])
  1335  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel])
  1336  	assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation])
  1337  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1338  	assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort)
  1339  	assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort)
  1340  	assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol)
  1341  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1342  }
  1343  
  1344  func TestGameServerPodHostName(t *testing.T) {
  1345  	t.Parallel()
  1346  
  1347  	fixture := defaultGameServer()
  1348  	fixture.ObjectMeta.Name = "test-1.0"
  1349  	fixture.ApplyDefaults()
  1350  	pod, err := fixture.Pod(fakeAPIHooks{})
  1351  	require.NoError(t, err)
  1352  	assert.Equal(t, "test-1-0", pod.Spec.Hostname)
  1353  
  1354  	fixture = defaultGameServer()
  1355  	fixture.ApplyDefaults()
  1356  	expected := "ORANGE"
  1357  	fixture.Spec.Template.Spec.Hostname = expected
  1358  	pod, err = fixture.Pod(fakeAPIHooks{})
  1359  	require.NoError(t, err)
  1360  	assert.Equal(t, expected, pod.Spec.Hostname)
  1361  }
  1362  
  1363  func TestGameServerPodContainerNotFoundErrReturned(t *testing.T) {
  1364  	t.Parallel()
  1365  
  1366  	containerName1 := "Container1"
  1367  	fixture := &GameServer{
  1368  		ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"},
  1369  		Spec: GameServerSpec{
  1370  			Container: "can-not-find-this-name",
  1371  			Ports: []GameServerPort{
  1372  				{
  1373  					Container:     &containerName1,
  1374  					ContainerPort: 7777,
  1375  					HostPort:      9999,
  1376  					PortPolicy:    Static,
  1377  				},
  1378  			},
  1379  			Template: corev1.PodTemplateSpec{
  1380  				Spec: corev1.PodSpec{
  1381  					Containers: []corev1.Container{{Name: "Container2", Image: "container/image"}},
  1382  				},
  1383  			},
  1384  		}, Status: GameServerStatus{State: GameServerStateCreating},
  1385  	}
  1386  
  1387  	_, err := fixture.Pod(fakeAPIHooks{})
  1388  	if assert.NotNil(t, err, "Pod should return an error") {
  1389  		assert.Equal(t, "failed to find container named Container1 in pod spec", err.Error())
  1390  	}
  1391  }
  1392  
  1393  func TestGameServerPodWithSidecarNoErrors(t *testing.T) {
  1394  	t.Parallel()
  1395  	runtime.FeatureTestMutex.Lock()
  1396  	defer runtime.FeatureTestMutex.Unlock()
  1397  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=false"))
  1398  
  1399  	fixture := defaultGameServer()
  1400  	fixture.ApplyDefaults()
  1401  
  1402  	sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"}
  1403  	fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk"
  1404  	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar)
  1405  	assert.Nil(t, err, "Pod should not return an error")
  1406  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1407  	assert.Len(t, pod.Spec.Containers, 2, "Should have two containers")
  1408  	assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName)
  1409  	assert.Equal(t, "sidecar", pod.Spec.Containers[0].Name)
  1410  	assert.Equal(t, "container", pod.Spec.Containers[1].Name)
  1411  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1412  }
  1413  
  1414  func TestGameServerPodWithInitSidecarNoErrors(t *testing.T) {
  1415  	t.Parallel()
  1416  	runtime.FeatureTestMutex.Lock()
  1417  	defer runtime.FeatureTestMutex.Unlock()
  1418  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true"))
  1419  
  1420  	fixture := defaultGameServer()
  1421  	fixture.ApplyDefaults()
  1422  
  1423  	sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"}
  1424  	fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk"
  1425  	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar)
  1426  	assert.Nil(t, err, "Pod should not return an error")
  1427  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1428  	assert.Len(t, pod.Spec.Containers, 1, "Should have one containers")
  1429  	assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName)
  1430  	assert.Equal(t, "sidecar", pod.Spec.InitContainers[0].Name)
  1431  	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy)
  1432  	assert.Equal(t, "container", pod.Spec.Containers[0].Name)
  1433  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1434  	assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever)
  1435  }
  1436  
  1437  func TestGameServerPodWithInitSidecarPrependsToExistingInitContainers(t *testing.T) {
  1438  	t.Parallel()
  1439  	runtime.FeatureTestMutex.Lock()
  1440  	defer runtime.FeatureTestMutex.Unlock()
  1441  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true"))
  1442  
  1443  	fixture := defaultGameServer()
  1444  	// Add existing init containers to the template
  1445  	existingInitContainer1 := corev1.Container{Name: "existing-init-1", Image: "existing/init1"}
  1446  	existingInitContainer2 := corev1.Container{Name: "existing-init-2", Image: "existing/init2"}
  1447  	fixture.Spec.Template.Spec.InitContainers = []corev1.Container{existingInitContainer1, existingInitContainer2}
  1448  	fixture.ApplyDefaults()
  1449  
  1450  	sidecar1 := corev1.Container{Name: "sidecar-1", Image: "container/sidecar1"}
  1451  	sidecar2 := corev1.Container{Name: "sidecar-2", Image: "container/sidecar2"}
  1452  	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar1, sidecar2)
  1453  	require.NoError(t, err)
  1454  
  1455  	// Verify that sidecars are placed at the beginning of InitContainers
  1456  	assert.Len(t, pod.Spec.InitContainers, 4, "Should have four init containers")
  1457  	assert.Equal(t, "sidecar-1", pod.Spec.InitContainers[0].Name, "First sidecar should be at index 0")
  1458  	assert.Equal(t, "sidecar-2", pod.Spec.InitContainers[1].Name, "Second sidecar should be at index 1")
  1459  	assert.Equal(t, "existing-init-1", pod.Spec.InitContainers[2].Name, "First existing init container should be at index 2")
  1460  	assert.Equal(t, "existing-init-2", pod.Spec.InitContainers[3].Name, "Second existing init container should be at index 3")
  1461  
  1462  	// Verify restart policies
  1463  	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy)
  1464  	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[1].RestartPolicy)
  1465  }
  1466  
  1467  func TestGameServerPodWithMultiplePortAllocations(t *testing.T) {
  1468  	fixture := defaultGameServer()
  1469  	containerName := "authContainer"
  1470  	fixture.Spec.Template.Spec.Containers = append(fixture.Spec.Template.Spec.Containers, corev1.Container{
  1471  		Name: containerName,
  1472  	})
  1473  	fixture.Spec.Ports = append(fixture.Spec.Ports, GameServerPort{
  1474  		Name:          "containerPort",
  1475  		PortPolicy:    Dynamic,
  1476  		Container:     &containerName,
  1477  		ContainerPort: 5000,
  1478  	})
  1479  	fixture.Spec.Container = fixture.Spec.Template.Spec.Containers[0].Name
  1480  	fixture.ApplyDefaults()
  1481  
  1482  	pod, err := fixture.Pod(fakeAPIHooks{})
  1483  	assert.NoError(t, err, "Pod should not return an error")
  1484  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1485  	assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace)
  1486  	assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"])
  1487  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel])
  1488  	assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation])
  1489  	assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort)
  1490  	assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort)
  1491  	assert.Equal(t, *fixture.Spec.Ports[0].Container, pod.Spec.Containers[0].Name)
  1492  	assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol)
  1493  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1494  	assert.Equal(t, fixture.Spec.Ports[1].HostPort, pod.Spec.Containers[1].Ports[0].HostPort)
  1495  	assert.Equal(t, fixture.Spec.Ports[1].ContainerPort, pod.Spec.Containers[1].Ports[0].ContainerPort)
  1496  	assert.Equal(t, *fixture.Spec.Ports[1].Container, pod.Spec.Containers[1].Name)
  1497  	assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[1].Ports[0].Protocol)
  1498  }
  1499  
  1500  func TestGameServerPodObjectMeta(t *testing.T) {
  1501  	fixture := &GameServer{
  1502  		ObjectMeta: metav1.ObjectMeta{Name: "lucy"},
  1503  		Spec:       GameServerSpec{Container: "goat"},
  1504  	}
  1505  
  1506  	for desc, tc := range map[string]struct {
  1507  		scheduling apis.SchedulingStrategy
  1508  		wantSafe   string
  1509  	}{
  1510  		"packed": {
  1511  			scheduling: apis.Packed,
  1512  		},
  1513  		"distributed": {
  1514  			scheduling: apis.Distributed,
  1515  		},
  1516  	} {
  1517  		t.Run(desc, func(t *testing.T) {
  1518  			gs := fixture.DeepCopy()
  1519  			gs.Spec.Scheduling = tc.scheduling
  1520  			pod := &corev1.Pod{}
  1521  
  1522  			gs.podObjectMeta(pod)
  1523  
  1524  			assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Name)
  1525  			assert.Equal(t, gs.ObjectMeta.Namespace, pod.ObjectMeta.Namespace)
  1526  			assert.Equal(t, GameServerLabelRole, pod.ObjectMeta.Labels[RoleLabel])
  1527  			assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"])
  1528  			assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel])
  1529  			assert.Equal(t, "goat", pod.ObjectMeta.Annotations[GameServerContainerAnnotation])
  1530  			assert.True(t, metav1.IsControlledBy(pod, gs))
  1531  			assert.Equal(t, tc.wantSafe, pod.ObjectMeta.Annotations[PodSafeToEvictAnnotation])
  1532  		})
  1533  	}
  1534  }
  1535  
  1536  func TestGameServerPodScheduling(t *testing.T) {
  1537  	fixture := &corev1.Pod{Spec: corev1.PodSpec{}}
  1538  
  1539  	t.Run("packed", func(t *testing.T) {
  1540  		gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Packed}}
  1541  		pod := fixture.DeepCopy()
  1542  		gs.podScheduling(pod)
  1543  
  1544  		assert.Len(t, pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, 1)
  1545  		wpat := pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0]
  1546  		assert.Equal(t, int32(100), wpat.Weight)
  1547  		assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), GameServerLabelRole)
  1548  		assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), RoleLabel)
  1549  	})
  1550  
  1551  	t.Run("distributed", func(t *testing.T) {
  1552  		gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Distributed}}
  1553  		pod := fixture.DeepCopy()
  1554  		gs.podScheduling(pod)
  1555  		assert.Empty(t, pod.Spec.Affinity)
  1556  	})
  1557  }
  1558  
  1559  func TestGameServerDisableServiceAccount(t *testing.T) {
  1560  	t.Parallel()
  1561  
  1562  	gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{
  1563  		Template: corev1.PodTemplateSpec{
  1564  			Spec: corev1.PodSpec{
  1565  				Containers: []corev1.Container{{Name: "container", Image: "container/image"}},
  1566  			},
  1567  		},
  1568  	}}
  1569  
  1570  	gs.ApplyDefaults()
  1571  	pod, err := gs.Pod(fakeAPIHooks{})
  1572  	assert.NoError(t, err)
  1573  	assert.Len(t, pod.Spec.Containers, 1)
  1574  	assert.Empty(t, pod.Spec.Containers[0].VolumeMounts)
  1575  
  1576  	err = gs.DisableServiceAccount(pod)
  1577  	assert.NoError(t, err)
  1578  	assert.Len(t, pod.Spec.Containers, 1)
  1579  	assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1)
  1580  	assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath)
  1581  }
  1582  
  1583  func TestGameServerPassthroughPortAnnotation(t *testing.T) {
  1584  	t.Parallel()
  1585  	runtime.FeatureTestMutex.Lock()
  1586  	defer runtime.FeatureTestMutex.Unlock()
  1587  	containerOne := "containerOne"
  1588  	containerTwo := "containerTwo"
  1589  	containerThree := "containerThree"
  1590  	containerFour := "containerFour"
  1591  	gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{
  1592  		Container: "containerOne",
  1593  		Ports: []GameServerPort{
  1594  			{Name: "defaultDynamicOne", PortPolicy: Dynamic, ContainerPort: 7659, Container: &containerOne},
  1595  			{Name: "defaultPassthroughOne", PortPolicy: Passthrough, Container: &containerOne},
  1596  			{Name: "defaultPassthroughTwo", PortPolicy: Passthrough, Container: &containerTwo},
  1597  			{Name: "defaultDynamicTwo", PortPolicy: Dynamic, ContainerPort: 7654, Container: &containerTwo},
  1598  			{Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7660, Container: &containerThree},
  1599  			{Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7661, Container: &containerThree},
  1600  			{Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7662, Container: &containerThree},
  1601  			{Name: "defaulPassthroughThree", PortPolicy: Passthrough, Container: &containerThree},
  1602  			{Name: "defaultPassthroughFour", PortPolicy: Passthrough, Container: &containerFour},
  1603  		},
  1604  		Template: corev1.PodTemplateSpec{
  1605  			Spec: corev1.PodSpec{
  1606  				Containers: []corev1.Container{
  1607  					{Name: "containerOne", Image: "container/image"},
  1608  					{Name: "containerTwo", Image: "container/image"},
  1609  					{Name: "containerThree", Image: "container/image"},
  1610  					{Name: "containerFour", Image: "container/image"},
  1611  				},
  1612  			},
  1613  		},
  1614  	}}
  1615  
  1616  	passthroughContainerPortMap := "{\"containerFour\":[0],\"containerOne\":[1],\"containerThree\":[3],\"containerTwo\":[0]}"
  1617  
  1618  	gs.ApplyDefaults()
  1619  	pod, err := gs.Pod(fakeAPIHooks{})
  1620  	assert.NoError(t, err)
  1621  	assert.Len(t, pod.Spec.Containers, 4)
  1622  	assert.Empty(t, pod.Spec.Containers[0].VolumeMounts)
  1623  	assert.Equal(t, pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation], passthroughContainerPortMap)
  1624  
  1625  	err = gs.DisableServiceAccount(pod)
  1626  	assert.NoError(t, err)
  1627  	assert.Len(t, pod.Spec.Containers, 4)
  1628  	assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1)
  1629  	assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath)
  1630  }
  1631  
  1632  func TestGameServerCountPorts(t *testing.T) {
  1633  	fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{
  1634  		{PortPolicy: Dynamic},
  1635  		{PortPolicy: Dynamic},
  1636  		{PortPolicy: Dynamic},
  1637  		{PortPolicy: Static},
  1638  	}}}
  1639  
  1640  	assert.Equal(t, 3, fixture.CountPorts(func(policy PortPolicy) bool {
  1641  		return policy == Dynamic
  1642  	}))
  1643  	assert.Equal(t, 1, fixture.CountPorts(func(policy PortPolicy) bool {
  1644  		return policy == Static
  1645  	}))
  1646  }
  1647  
  1648  func TestGameServerCountPortsForRange(t *testing.T) {
  1649  	fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{
  1650  		{PortPolicy: Dynamic, Range: "test"},
  1651  		{PortPolicy: Dynamic},
  1652  		{PortPolicy: Dynamic, Range: "test"},
  1653  		{PortPolicy: Static, Range: "test"},
  1654  	}}}
  1655  
  1656  	assert.Equal(t, 2, fixture.CountPortsForRange("test", func(policy PortPolicy) bool {
  1657  		return policy == Dynamic
  1658  	}))
  1659  	assert.Equal(t, 1, fixture.CountPortsForRange("test", func(policy PortPolicy) bool {
  1660  		return policy == Static
  1661  	}))
  1662  }
  1663  
  1664  func TestGameServerPatch(t *testing.T) {
  1665  	fixture := &GameServer{
  1666  		ObjectMeta: metav1.ObjectMeta{Name: "lucy", ResourceVersion: "1234"},
  1667  		Spec:       GameServerSpec{Container: "goat"},
  1668  	}
  1669  
  1670  	delta := fixture.DeepCopy()
  1671  	delta.Spec.Container = "bear"
  1672  
  1673  	patch, err := fixture.Patch(delta)
  1674  	assert.Nil(t, err)
  1675  
  1676  	assert.Contains(t, string(patch), `{"op":"replace","path":"/spec/container","value":"bear"}`)
  1677  	assert.Contains(t, string(patch), `{"op":"test","path":"/metadata/resourceVersion","value":"1234"}`)
  1678  }
  1679  
  1680  func TestGameServerGetDevAddress(t *testing.T) {
  1681  	devGs := &GameServer{
  1682  		ObjectMeta: metav1.ObjectMeta{
  1683  			Name:        "dev-game",
  1684  			Namespace:   "default",
  1685  			Annotations: map[string]string{DevAddressAnnotation: ipFixture},
  1686  		},
  1687  		Spec: GameServerSpec{
  1688  			Ports: []GameServerPort{{HostPort: 7777, PortPolicy: Static}},
  1689  			Template: corev1.PodTemplateSpec{
  1690  				Spec: corev1.PodSpec{
  1691  					Containers: []corev1.Container{{Name: "container", Image: "container/image"}},
  1692  				},
  1693  			},
  1694  		},
  1695  	}
  1696  
  1697  	devAddress, isDev := devGs.GetDevAddress()
  1698  	assert.True(t, isDev, "dev-game should had a dev-address")
  1699  	assert.Equal(t, ipFixture, devAddress, "dev-address IP address should be 127.1.1.1")
  1700  
  1701  	regularGs := devGs.DeepCopy()
  1702  	regularGs.ObjectMeta.Annotations = map[string]string{}
  1703  	devAddress, isDev = regularGs.GetDevAddress()
  1704  	assert.False(t, isDev, "dev-game should NOT have a dev-address")
  1705  	assert.Equal(t, "", devAddress, "dev-address IP address should be 127.1.1.1")
  1706  }
  1707  
  1708  func TestGameServerIsDeletable(t *testing.T) {
  1709  	gs := &GameServer{Status: GameServerStatus{State: GameServerStateStarting}}
  1710  	assert.True(t, gs.IsDeletable())
  1711  
  1712  	gs.Status.State = GameServerStateAllocated
  1713  	assert.False(t, gs.IsDeletable())
  1714  
  1715  	gs.Status.State = GameServerStateReserved
  1716  	assert.False(t, gs.IsDeletable())
  1717  
  1718  	now := metav1.Now()
  1719  	gs.ObjectMeta.DeletionTimestamp = &now
  1720  	assert.True(t, gs.IsDeletable())
  1721  
  1722  	gs.Status.State = GameServerStateAllocated
  1723  	assert.True(t, gs.IsDeletable())
  1724  
  1725  	gs.Status.State = GameServerStateReady
  1726  	assert.True(t, gs.IsDeletable())
  1727  }
  1728  
  1729  func TestGameServerIsBeforeReady(t *testing.T) {
  1730  	fixtures := []struct {
  1731  		state    GameServerState
  1732  		expected bool
  1733  	}{
  1734  		{GameServerStatePortAllocation, true},
  1735  		{GameServerStateCreating, true},
  1736  		{GameServerStateStarting, true},
  1737  		{GameServerStateScheduled, true},
  1738  		{GameServerStateRequestReady, true},
  1739  		{GameServerStateReady, false},
  1740  		{GameServerStateShutdown, false},
  1741  		{GameServerStateError, false},
  1742  		{GameServerStateUnhealthy, false},
  1743  		{GameServerStateReserved, false},
  1744  		{GameServerStateAllocated, false},
  1745  	}
  1746  
  1747  	for _, test := range fixtures {
  1748  		t.Run(string(test.state), func(t *testing.T) {
  1749  			gs := &GameServer{Status: GameServerStatus{State: test.state}}
  1750  			assert.Equal(t, test.expected, gs.IsBeforeReady(), test.state)
  1751  		})
  1752  	}
  1753  }
  1754  
  1755  func TestGameServerApplyToPodContainer(t *testing.T) {
  1756  	t.Parallel()
  1757  	type expected struct {
  1758  		err string
  1759  		tty bool
  1760  	}
  1761  
  1762  	testCases := []struct {
  1763  		description string
  1764  		gs          *GameServer
  1765  		expected    expected
  1766  	}{
  1767  		{
  1768  			description: "OK, no error",
  1769  			gs: &GameServer{
  1770  				Spec: GameServerSpec{
  1771  					Container: "mycontainer",
  1772  					Template: corev1.PodTemplateSpec{
  1773  						Spec: corev1.PodSpec{
  1774  							Containers: []corev1.Container{
  1775  								{Name: "mycontainer", Image: "foo/mycontainer"},
  1776  								{Name: "notmycontainer", Image: "foo/notmycontainer"},
  1777  							},
  1778  						},
  1779  					},
  1780  				},
  1781  			},
  1782  			expected: expected{
  1783  				err: "",
  1784  				tty: true,
  1785  			},
  1786  		},
  1787  		{
  1788  			description: "container not found, error is returned",
  1789  			gs: &GameServer{
  1790  				Spec: GameServerSpec{
  1791  					Container: "mycontainer-WRONG-NAME",
  1792  					Template: corev1.PodTemplateSpec{
  1793  						Spec: corev1.PodSpec{
  1794  							Containers: []corev1.Container{
  1795  								{Name: "mycontainer", Image: "foo/mycontainer"},
  1796  								{Name: "notmycontainer", Image: "foo/notmycontainer"},
  1797  							},
  1798  						},
  1799  					},
  1800  				},
  1801  			},
  1802  			expected: expected{
  1803  				err: "failed to find container named mycontainer-WRONG-NAME in pod spec",
  1804  				tty: false,
  1805  			},
  1806  		},
  1807  	}
  1808  
  1809  	for _, tc := range testCases {
  1810  		t.Run(tc.description, func(t *testing.T) {
  1811  			pod := &corev1.Pod{Spec: *tc.gs.Spec.Template.Spec.DeepCopy()}
  1812  			result := tc.gs.ApplyToPodContainer(pod, tc.gs.Spec.Container, func(c corev1.Container) corev1.Container {
  1813  				//  easy thing to change and test for
  1814  				c.TTY = true
  1815  				return c
  1816  			})
  1817  
  1818  			if tc.expected.err != "" && assert.NotNil(t, result) {
  1819  				assert.Equal(t, tc.expected.err, result.Error())
  1820  			}
  1821  			assert.Equal(t, tc.expected.tty, pod.Spec.Containers[0].TTY)
  1822  			assert.False(t, pod.Spec.Containers[1].TTY)
  1823  		})
  1824  	}
  1825  }
  1826  
  1827  func defaultGameServer() *GameServer {
  1828  	return &GameServer{
  1829  		ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"},
  1830  		Spec: GameServerSpec{
  1831  			Ports: []GameServerPort{
  1832  				{
  1833  					ContainerPort: 7777,
  1834  					HostPort:      9999,
  1835  					PortPolicy:    Static,
  1836  				},
  1837  			},
  1838  			Template: corev1.PodTemplateSpec{
  1839  				Spec: corev1.PodSpec{
  1840  					Containers: []corev1.Container{{Name: "container", Image: "container/image"}},
  1841  				},
  1842  			},
  1843  		}, Status: GameServerStatus{State: GameServerStateCreating},
  1844  	}
  1845  }
  1846  
  1847  func TestGameServerUpdateCount(t *testing.T) {
  1848  	t.Parallel()
  1849  
  1850  	testCases := map[string]struct {
  1851  		gs      GameServer
  1852  		name    string
  1853  		action  string
  1854  		amount  int64
  1855  		want    CounterStatus
  1856  		wantErr bool
  1857  	}{
  1858  		"counter not in game server no-op and error": {
  1859  			gs: GameServer{Status: GameServerStatus{
  1860  				Counters: map[string]CounterStatus{
  1861  					"foos": {
  1862  						Count:    0,
  1863  						Capacity: 100,
  1864  					},
  1865  				},
  1866  			}},
  1867  			name:    "foo",
  1868  			action:  "Increment",
  1869  			amount:  1,
  1870  			wantErr: true,
  1871  		},
  1872  		"negative amount no-op and error": {
  1873  			gs: GameServer{Status: GameServerStatus{
  1874  				Counters: map[string]CounterStatus{
  1875  					"foos": {
  1876  						Count:    1,
  1877  						Capacity: 100,
  1878  					},
  1879  				},
  1880  			}},
  1881  			name:   "foos",
  1882  			action: "Decrement",
  1883  			amount: -1,
  1884  			want: CounterStatus{
  1885  				Count:    1,
  1886  				Capacity: 100,
  1887  			},
  1888  			wantErr: true,
  1889  		},
  1890  		"increment by 1": {
  1891  			gs: GameServer{Status: GameServerStatus{
  1892  				Counters: map[string]CounterStatus{
  1893  					"players": {
  1894  						Count:    0,
  1895  						Capacity: 100,
  1896  					},
  1897  				},
  1898  			}},
  1899  			name:   "players",
  1900  			action: "Increment",
  1901  			amount: 1,
  1902  			want: CounterStatus{
  1903  				Count:    1,
  1904  				Capacity: 100,
  1905  			},
  1906  			wantErr: false,
  1907  		},
  1908  		"decrement by 10": {
  1909  			gs: GameServer{Status: GameServerStatus{
  1910  				Counters: map[string]CounterStatus{
  1911  					"bars": {
  1912  						Count:    99,
  1913  						Capacity: 100,
  1914  					},
  1915  				},
  1916  			}},
  1917  			name:   "bars",
  1918  			action: "Decrement",
  1919  			amount: 10,
  1920  			want: CounterStatus{
  1921  				Count:    89,
  1922  				Capacity: 100,
  1923  			},
  1924  			wantErr: false,
  1925  		},
  1926  		"incorrect action no-op and error": {
  1927  			gs: GameServer{Status: GameServerStatus{
  1928  				Counters: map[string]CounterStatus{
  1929  					"bazes": {
  1930  						Count:    99,
  1931  						Capacity: 100,
  1932  					},
  1933  				},
  1934  			}},
  1935  			name:   "bazes",
  1936  			action: "decrement",
  1937  			amount: 10,
  1938  			want: CounterStatus{
  1939  				Count:    99,
  1940  				Capacity: 100,
  1941  			},
  1942  			wantErr: true,
  1943  		},
  1944  		"decrement beyond zero truncated": {
  1945  			gs: GameServer{Status: GameServerStatus{
  1946  				Counters: map[string]CounterStatus{
  1947  					"baz": {
  1948  						Count:    99,
  1949  						Capacity: 100,
  1950  					},
  1951  				},
  1952  			}},
  1953  			name:   "baz",
  1954  			action: "Decrement",
  1955  			amount: 100,
  1956  			want: CounterStatus{
  1957  				Count:    0,
  1958  				Capacity: 100,
  1959  			},
  1960  			wantErr: false,
  1961  		},
  1962  		"increment beyond capacity truncated": {
  1963  			gs: GameServer{Status: GameServerStatus{
  1964  				Counters: map[string]CounterStatus{
  1965  					"splayers": {
  1966  						Count:    99,
  1967  						Capacity: 100,
  1968  					},
  1969  				},
  1970  			}},
  1971  			name:   "splayers",
  1972  			action: "Increment",
  1973  			amount: 2,
  1974  			want: CounterStatus{
  1975  				Count:    100,
  1976  				Capacity: 100,
  1977  			},
  1978  			wantErr: false,
  1979  		},
  1980  	}
  1981  
  1982  	for test, testCase := range testCases {
  1983  		t.Run(test, func(t *testing.T) {
  1984  			err := testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount)
  1985  			if err != nil {
  1986  				assert.True(t, testCase.wantErr)
  1987  			} else {
  1988  				assert.False(t, testCase.wantErr)
  1989  			}
  1990  			if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
  1991  				assert.Equal(t, testCase.want, counter)
  1992  			}
  1993  		})
  1994  	}
  1995  }
  1996  
  1997  func TestGameServerUpdateCounterCapacity(t *testing.T) {
  1998  	t.Parallel()
  1999  
  2000  	testCases := map[string]struct {
  2001  		gs       GameServer
  2002  		name     string
  2003  		capacity int64
  2004  		want     CounterStatus
  2005  		wantErr  bool
  2006  	}{
  2007  		"counter not in game server no-op with error": {
  2008  			gs: GameServer{Status: GameServerStatus{
  2009  				Counters: map[string]CounterStatus{
  2010  					"foos": {
  2011  						Count:    0,
  2012  						Capacity: 100,
  2013  					},
  2014  				},
  2015  			}},
  2016  			name:     "foo",
  2017  			capacity: 1000,
  2018  			wantErr:  true,
  2019  		},
  2020  		"capacity less than zero no-op with error": {
  2021  			gs: GameServer{Status: GameServerStatus{
  2022  				Counters: map[string]CounterStatus{
  2023  					"foos": {
  2024  						Count:    0,
  2025  						Capacity: 100,
  2026  					},
  2027  				},
  2028  			}},
  2029  			name:     "foos",
  2030  			capacity: -1000,
  2031  			want: CounterStatus{
  2032  				Count:    0,
  2033  				Capacity: 100,
  2034  			},
  2035  			wantErr: true,
  2036  		},
  2037  		"update capacity": {
  2038  			gs: GameServer{Status: GameServerStatus{
  2039  				Counters: map[string]CounterStatus{
  2040  					"sessions": {
  2041  						Count:    0,
  2042  						Capacity: 100,
  2043  					},
  2044  				},
  2045  			}},
  2046  			name:     "sessions",
  2047  			capacity: 9223372036854775807,
  2048  			want: CounterStatus{
  2049  				Count:    0,
  2050  				Capacity: 9223372036854775807,
  2051  			},
  2052  		},
  2053  	}
  2054  
  2055  	for test, testCase := range testCases {
  2056  		t.Run(test, func(t *testing.T) {
  2057  			err := testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity)
  2058  			if err != nil {
  2059  				assert.True(t, testCase.wantErr)
  2060  			} else {
  2061  				assert.False(t, testCase.wantErr)
  2062  			}
  2063  			if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
  2064  				assert.Equal(t, testCase.want, counter)
  2065  			}
  2066  		})
  2067  	}
  2068  }
  2069  
  2070  func TestGameServerUpdateListCapacity(t *testing.T) {
  2071  	t.Parallel()
  2072  
  2073  	testCases := map[string]struct {
  2074  		gs       GameServer
  2075  		name     string
  2076  		capacity int64
  2077  		want     ListStatus
  2078  		wantErr  bool
  2079  	}{
  2080  		"list not in game server no-op with error": {
  2081  			gs: GameServer{Status: GameServerStatus{
  2082  				Lists: map[string]ListStatus{
  2083  					"things": {
  2084  						Values:   []string{},
  2085  						Capacity: 100,
  2086  					},
  2087  				},
  2088  			}},
  2089  			name:     "thing",
  2090  			capacity: 1000,
  2091  			wantErr:  true,
  2092  		},
  2093  		"update list capacity": {
  2094  			gs: GameServer{Status: GameServerStatus{
  2095  				Lists: map[string]ListStatus{
  2096  					"things": {
  2097  						Values:   []string{},
  2098  						Capacity: 100,
  2099  					},
  2100  				},
  2101  			}},
  2102  			name:     "things",
  2103  			capacity: 1000,
  2104  			want: ListStatus{
  2105  				Values:   []string{},
  2106  				Capacity: 1000,
  2107  			},
  2108  			wantErr: false,
  2109  		},
  2110  		"list capacity above max no-op with error": {
  2111  			gs: GameServer{Status: GameServerStatus{
  2112  				Lists: map[string]ListStatus{
  2113  					"slings": {
  2114  						Values:   []string{},
  2115  						Capacity: 100,
  2116  					},
  2117  				},
  2118  			}},
  2119  			name:     "slings",
  2120  			capacity: 10000,
  2121  			want: ListStatus{
  2122  				Values:   []string{},
  2123  				Capacity: 100,
  2124  			},
  2125  			wantErr: true,
  2126  		},
  2127  		"list capacity less than zero no-op with error": {
  2128  			gs: GameServer{Status: GameServerStatus{
  2129  				Lists: map[string]ListStatus{
  2130  					"flings": {
  2131  						Values:   []string{},
  2132  						Capacity: 999,
  2133  					},
  2134  				},
  2135  			}},
  2136  			name:     "flings",
  2137  			capacity: -100,
  2138  			want: ListStatus{
  2139  				Values:   []string{},
  2140  				Capacity: 999,
  2141  			},
  2142  			wantErr: true,
  2143  		},
  2144  	}
  2145  
  2146  	for test, testCase := range testCases {
  2147  		t.Run(test, func(t *testing.T) {
  2148  			err := testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity)
  2149  			if err != nil {
  2150  				assert.True(t, testCase.wantErr)
  2151  			} else {
  2152  				assert.False(t, testCase.wantErr)
  2153  			}
  2154  			if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
  2155  				assert.Equal(t, testCase.want, list)
  2156  			}
  2157  		})
  2158  	}
  2159  }
  2160  
  2161  func TestGameServerAppendListValues(t *testing.T) {
  2162  	t.Parallel()
  2163  
  2164  	var nilSlice []string
  2165  
  2166  	testCases := map[string]struct {
  2167  		gs      GameServer
  2168  		name    string
  2169  		values  []string
  2170  		want    ListStatus
  2171  		wantErr bool
  2172  	}{
  2173  		"list not in game server no-op with error": {
  2174  			gs: GameServer{Status: GameServerStatus{
  2175  				Lists: map[string]ListStatus{
  2176  					"things": {
  2177  						Values:   []string{},
  2178  						Capacity: 100,
  2179  					},
  2180  				},
  2181  			}},
  2182  			name:    "thing",
  2183  			values:  []string{"thing1", "thing2", "thing3"},
  2184  			wantErr: true,
  2185  		},
  2186  		"append values": {
  2187  			gs: GameServer{Status: GameServerStatus{
  2188  				Lists: map[string]ListStatus{
  2189  					"things": {
  2190  						Values:   []string{"thing1"},
  2191  						Capacity: 100,
  2192  					},
  2193  				},
  2194  			}},
  2195  			name:   "things",
  2196  			values: []string{"thing2", "thing3"},
  2197  			want: ListStatus{
  2198  				Values:   []string{"thing1", "thing2", "thing3"},
  2199  				Capacity: 100,
  2200  			},
  2201  			wantErr: false,
  2202  		},
  2203  		"append values with silent drop of duplicates": {
  2204  			gs: GameServer{Status: GameServerStatus{
  2205  				Lists: map[string]ListStatus{
  2206  					"games": {
  2207  						Values:   []string{"game0"},
  2208  						Capacity: 10,
  2209  					},
  2210  				},
  2211  			}},
  2212  			name:   "games",
  2213  			values: []string{"game1", "game2", "game2", "game1"},
  2214  			want: ListStatus{
  2215  				Values:   []string{"game0", "game1", "game2"},
  2216  				Capacity: 10,
  2217  			},
  2218  			wantErr: false,
  2219  		},
  2220  		"append values with silent drop of duplicates in original list": {
  2221  			gs: GameServer{Status: GameServerStatus{
  2222  				Lists: map[string]ListStatus{
  2223  					"objects": {
  2224  						Values:   []string{"object1", "object2"},
  2225  						Capacity: 10,
  2226  					},
  2227  				},
  2228  			}},
  2229  			name:   "objects",
  2230  			values: []string{"object2", "object1", "object3", "object3"},
  2231  			want: ListStatus{
  2232  				Values:   []string{"object1", "object2", "object3"},
  2233  				Capacity: 10,
  2234  			},
  2235  			wantErr: false,
  2236  		},
  2237  		"append nil values": {
  2238  			gs: GameServer{Status: GameServerStatus{
  2239  				Lists: map[string]ListStatus{
  2240  					"blings": {
  2241  						Values:   []string{"bling1"},
  2242  						Capacity: 10,
  2243  					},
  2244  				},
  2245  			}},
  2246  			name:   "blings",
  2247  			values: nilSlice,
  2248  			want: ListStatus{
  2249  				Values:   []string{"bling1"},
  2250  				Capacity: 10,
  2251  			},
  2252  			wantErr: true,
  2253  		},
  2254  		"append too many values truncates list": {
  2255  			gs: GameServer{Status: GameServerStatus{
  2256  				Lists: map[string]ListStatus{
  2257  					"bananaslugs": {
  2258  						Values:   []string{"bananaslugs1", "bananaslug2", "bananaslug3"},
  2259  						Capacity: 5,
  2260  					},
  2261  				},
  2262  			}},
  2263  			name:   "bananaslugs",
  2264  			values: []string{"bananaslug4", "bananaslug5", "bananaslug6"},
  2265  			want: ListStatus{
  2266  				Values:   []string{"bananaslugs1", "bananaslug2", "bananaslug3", "bananaslug4", "bananaslug5"},
  2267  				Capacity: 5,
  2268  			},
  2269  			wantErr: false,
  2270  		},
  2271  	}
  2272  
  2273  	for test, testCase := range testCases {
  2274  		t.Run(test, func(t *testing.T) {
  2275  			err := testCase.gs.AppendListValues(testCase.name, testCase.values)
  2276  			if err != nil {
  2277  				assert.True(t, testCase.wantErr)
  2278  			} else {
  2279  				assert.False(t, testCase.wantErr)
  2280  			}
  2281  			if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
  2282  				assert.Equal(t, testCase.want, list)
  2283  			}
  2284  		})
  2285  	}
  2286  }
  2287  
  2288  func TestGameServerDeleteListValues(t *testing.T) {
  2289  	t.Parallel()
  2290  
  2291  	testCases := map[string]struct {
  2292  		gs      GameServer
  2293  		name    string
  2294  		want    ListStatus
  2295  		values  []string
  2296  		wantErr bool
  2297  	}{
  2298  		"list not in game server no-op and error": {
  2299  			gs: GameServer{Status: GameServerStatus{
  2300  				Lists: map[string]ListStatus{
  2301  					"foos": {
  2302  						Values:   []string{"foo", "bar", "bax"},
  2303  						Capacity: 100,
  2304  					},
  2305  				},
  2306  			}},
  2307  			name:    "foo",
  2308  			values:  []string{"bar", "baz"},
  2309  			wantErr: true,
  2310  		},
  2311  		"delete list value - one value not present": {
  2312  			gs: GameServer{Status: GameServerStatus{
  2313  				Lists: map[string]ListStatus{
  2314  					"foo": {
  2315  						Values:   []string{"foo", "bar", "bax"},
  2316  						Capacity: 100,
  2317  					},
  2318  				},
  2319  			}},
  2320  			name:    "foo",
  2321  			values:  []string{"bar", "baz"},
  2322  			wantErr: false,
  2323  			want: ListStatus{
  2324  				Values:   []string{"foo", "bax"},
  2325  				Capacity: 100,
  2326  			},
  2327  		},
  2328  	}
  2329  
  2330  	for test, testCase := range testCases {
  2331  		t.Run(test, func(t *testing.T) {
  2332  			err := testCase.gs.DeleteListValues(testCase.name, testCase.values)
  2333  			if err != nil {
  2334  				assert.True(t, testCase.wantErr)
  2335  			} else {
  2336  				assert.False(t, testCase.wantErr)
  2337  			}
  2338  			if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
  2339  				assert.Equal(t, testCase.want, list)
  2340  			}
  2341  		})
  2342  	}
  2343  }
  2344  
  2345  func TestMergeRemoveDuplicates(t *testing.T) {
  2346  	t.Parallel()
  2347  
  2348  	testCases := map[string]struct {
  2349  		str1 []string
  2350  		str2 []string
  2351  		want []string
  2352  	}{
  2353  		"empty string arrays": {
  2354  			str1: []string{},
  2355  			str2: []string{},
  2356  			want: []string{},
  2357  		},
  2358  		"no duplicates": {
  2359  			str1: []string{"one"},
  2360  			str2: []string{"two", "three"},
  2361  			want: []string{"one", "two", "three"},
  2362  		},
  2363  		"remove one duplicate": {
  2364  			str1: []string{"one", "one", "one"},
  2365  			str2: []string{"one", "one", "one"},
  2366  			want: []string{"one"},
  2367  		},
  2368  		"remove multiple duplicates": {
  2369  			str1: []string{"one", "two"},
  2370  			str2: []string{"two", "one"},
  2371  			want: []string{"one", "two"},
  2372  		},
  2373  	}
  2374  
  2375  	for test, testCase := range testCases {
  2376  		t.Run(test, func(t *testing.T) {
  2377  			got := MergeRemoveDuplicates(testCase.str1, testCase.str2)
  2378  			assert.Equal(t, testCase.want, got)
  2379  		})
  2380  	}
  2381  }