agones.dev/agones@v1.53.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.Invalid(field.NewPath("spec", "template", "metadata", "labels"), longNameLen64, "name part must be no more than 63 characters"),
   611  			},
   612  		},
   613  		{
   614  			description: "Long label value is invalid",
   615  			gs: GameServer{
   616  				ObjectMeta: metav1.ObjectMeta{
   617  					GenerateName: "ok-name",
   618  				},
   619  				TypeMeta: metav1.TypeMeta{
   620  					Kind: "test-kind",
   621  				},
   622  				Spec: GameServerSpec{
   623  					Container: "my_image",
   624  					Template: corev1.PodTemplateSpec{
   625  						Spec: corev1.PodSpec{
   626  							Containers: []corev1.Container{
   627  								{Name: "my_image", Image: "foo/my_image"},
   628  							},
   629  						},
   630  						ObjectMeta: metav1.ObjectMeta{
   631  							Labels: map[string]string{"agones.dev/longValueKey": longNameLen64},
   632  						},
   633  					},
   634  				},
   635  			},
   636  			applyDefaults: false,
   637  			want: field.ErrorList{
   638  				field.Invalid(field.NewPath("spec", "template", "metadata", "labels"), longNameLen64, "must be no more than 63 characters"),
   639  			},
   640  		},
   641  		{
   642  			description: "Long annotation key is invalid",
   643  			gs: GameServer{
   644  				ObjectMeta: metav1.ObjectMeta{
   645  					GenerateName: "ok-name",
   646  				},
   647  				TypeMeta: metav1.TypeMeta{
   648  					Kind: "test-kind",
   649  				},
   650  				Spec: GameServerSpec{
   651  					Container: "my_image",
   652  					Template: corev1.PodTemplateSpec{
   653  						Spec: corev1.PodSpec{
   654  							Containers: []corev1.Container{
   655  								{Name: "my_image", Image: "foo/my_image"},
   656  							},
   657  						},
   658  						ObjectMeta: metav1.ObjectMeta{
   659  							Annotations: map[string]string{longNameLen64: longNameLen64},
   660  						},
   661  					},
   662  				},
   663  			},
   664  			applyDefaults: false,
   665  			want: field.ErrorList{
   666  				field.Invalid(field.NewPath("spec", "template", "metadata", "annotations"), longNameLen64, "name part must be no more than 63 characters"),
   667  			},
   668  		},
   669  		{
   670  			description: "Invalid character in annotation key",
   671  			gs: GameServer{
   672  				ObjectMeta: metav1.ObjectMeta{
   673  					GenerateName: "ok-name",
   674  				},
   675  				TypeMeta: metav1.TypeMeta{
   676  					Kind: "test-kind",
   677  				},
   678  				Spec: GameServerSpec{
   679  					Container: "my_image",
   680  					Template: corev1.PodTemplateSpec{
   681  						Spec: corev1.PodSpec{
   682  							Containers: []corev1.Container{
   683  								{Name: "my_image", Image: "foo/my_image"},
   684  							},
   685  						},
   686  						ObjectMeta: metav1.ObjectMeta{
   687  							Annotations: map[string]string{"agones.dev/short±Name": longNameLen64},
   688  						},
   689  					},
   690  				},
   691  			},
   692  			applyDefaults: false,
   693  			want: field.ErrorList{
   694  				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]')"),
   695  			},
   696  		},
   697  		{
   698  			description: "Valid annotation key",
   699  			gs: GameServer{
   700  				ObjectMeta: metav1.ObjectMeta{
   701  					GenerateName: "ok-name",
   702  				},
   703  				TypeMeta: metav1.TypeMeta{
   704  					Kind: "test-kind",
   705  				},
   706  				Spec: GameServerSpec{
   707  					Container: "my_image",
   708  					Template: corev1.PodTemplateSpec{
   709  						Spec: corev1.PodSpec{
   710  							Containers: []corev1.Container{
   711  								{Name: "my_image", Image: "foo/my_image"},
   712  							},
   713  						},
   714  						ObjectMeta: metav1.ObjectMeta{
   715  							Annotations: map[string]string{"agones.dev/shortName": longNameLen64},
   716  						},
   717  					},
   718  				},
   719  			},
   720  			applyDefaults: false,
   721  		},
   722  		{
   723  			description: "Check ContainerPort and HostPort with different policies",
   724  			gs: GameServer{
   725  				ObjectMeta: metav1.ObjectMeta{
   726  					GenerateName: "ok-name",
   727  				},
   728  				TypeMeta: metav1.TypeMeta{
   729  					Kind: "test-kind",
   730  				},
   731  				Spec: GameServerSpec{
   732  					Ports: []GameServerPort{
   733  						{Name: "one", PortPolicy: Passthrough, ContainerPort: 1294},
   734  						{PortPolicy: Passthrough, Name: "two", HostPort: 7890},
   735  						{PortPolicy: Dynamic, Name: "three", HostPort: 7890, ContainerPort: 1294},
   736  					},
   737  					Container: "my_image",
   738  					Template: corev1.PodTemplateSpec{
   739  						Spec: corev1.PodSpec{
   740  							Containers: []corev1.Container{
   741  								{Name: "my_image", Image: "foo/my_image"},
   742  							},
   743  						},
   744  					},
   745  				},
   746  			},
   747  			applyDefaults: true,
   748  			want: field.ErrorList{
   749  				field.Required(field.NewPath("spec", "ports").Index(0).Child("containerPort"), "ContainerPort cannot be specified with Passthrough PortPolicy"),
   750  				field.Forbidden(field.NewPath("spec", "ports").Index(1).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"),
   751  				field.Forbidden(field.NewPath("spec", "ports").Index(2).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"),
   752  			},
   753  		},
   754  		{
   755  			description: "PortPolicy must be Static with HostPort specified",
   756  			gs: GameServer{
   757  				ObjectMeta: metav1.ObjectMeta{
   758  					Name:        "dev-game",
   759  					Namespace:   "default",
   760  					Annotations: map[string]string{DevAddressAnnotation: ipFixture},
   761  				},
   762  				Spec: GameServerSpec{
   763  					Ports: []GameServerPort{
   764  						{PortPolicy: Passthrough, Name: "main", HostPort: 7890, ContainerPort: 7777},
   765  					},
   766  					Container: "my_image",
   767  					Template: corev1.PodTemplateSpec{
   768  						Spec: corev1.PodSpec{
   769  							Containers: []corev1.Container{
   770  								{Name: "my_image", Image: "foo/my_image"},
   771  							},
   772  						},
   773  					},
   774  				},
   775  			},
   776  			applyDefaults: true,
   777  			want: field.ErrorList{
   778  				field.Required(
   779  					field.NewPath("spec", "ports").Index(0).Child("portPolicy"),
   780  					"PortPolicy must be Static"),
   781  			},
   782  		},
   783  		{
   784  			description: "ContainerPort is less than zero",
   785  			gs: GameServer{
   786  				ObjectMeta: metav1.ObjectMeta{
   787  					Name:      "dev-game",
   788  					Namespace: "default",
   789  				},
   790  				Spec: GameServerSpec{
   791  					Ports: []GameServerPort{{
   792  						Name:          "main",
   793  						ContainerPort: -4,
   794  						PortPolicy:    Dynamic,
   795  					}},
   796  					Container: "testing",
   797  					Template: corev1.PodTemplateSpec{
   798  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   799  							{Name: "testing", Image: "testing/image"},
   800  						}},
   801  					},
   802  				},
   803  			},
   804  			applyDefaults: false,
   805  			want: field.ErrorList{
   806  				field.Required(
   807  					field.NewPath("spec", "ports").Index(0).Child("containerPort"),
   808  					"ContainerPort must be defined for Dynamic and Static PortPolicies",
   809  				),
   810  			},
   811  		},
   812  		{
   813  			description: "CPU Request > Limit",
   814  			gs: GameServer{
   815  				ObjectMeta: metav1.ObjectMeta{
   816  					Name:      "dev-game",
   817  					Namespace: "default",
   818  				},
   819  				Spec: GameServerSpec{
   820  					Ports: []GameServerPort{{
   821  						Name:          "main",
   822  						ContainerPort: 7777,
   823  						PortPolicy:    Dynamic,
   824  					}},
   825  					Container: "testing",
   826  					Template: corev1.PodTemplateSpec{
   827  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   828  							{
   829  								Name:  "testing",
   830  								Image: "testing/image",
   831  								Resources: corev1.ResourceRequirements{
   832  									Requests: corev1.ResourceList{
   833  										corev1.ResourceCPU:    resource.MustParse("50m"),
   834  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   835  									},
   836  									Limits: corev1.ResourceList{
   837  										corev1.ResourceCPU:    resource.MustParse("30m"),
   838  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   839  									},
   840  								},
   841  							},
   842  						}},
   843  					},
   844  				},
   845  			},
   846  			applyDefaults: false,
   847  			want: field.ErrorList{
   848  				field.Invalid(
   849  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
   850  					"50m",
   851  					"must be less than or equal to cpu limit of 30m",
   852  				),
   853  			},
   854  		},
   855  		{
   856  			description: "CPU negative request",
   857  			gs: GameServer{
   858  				ObjectMeta: metav1.ObjectMeta{
   859  					Name:      "dev-game",
   860  					Namespace: "default",
   861  				},
   862  				Spec: GameServerSpec{
   863  					Ports: []GameServerPort{{
   864  						Name:          "main",
   865  						ContainerPort: 7777,
   866  						PortPolicy:    Dynamic,
   867  					}},
   868  					Container: "testing",
   869  					Template: corev1.PodTemplateSpec{
   870  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   871  							{
   872  								Name:  "testing",
   873  								Image: "testing/image",
   874  								Resources: corev1.ResourceRequirements{
   875  									Requests: corev1.ResourceList{
   876  										corev1.ResourceCPU:    resource.MustParse("-30m"),
   877  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   878  									},
   879  									Limits: corev1.ResourceList{
   880  										corev1.ResourceCPU:    resource.MustParse("30m"),
   881  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   882  									},
   883  								},
   884  							},
   885  						}},
   886  					},
   887  				},
   888  			},
   889  			applyDefaults: false,
   890  			want: field.ErrorList{
   891  				field.Invalid(
   892  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("cpu"),
   893  					"-30m",
   894  					"must be greater than or equal to 0",
   895  				),
   896  			},
   897  		},
   898  		{
   899  			description: "CPU negative limit",
   900  			gs: GameServer{
   901  				ObjectMeta: metav1.ObjectMeta{
   902  					Name:      "dev-game",
   903  					Namespace: "default",
   904  				},
   905  				Spec: GameServerSpec{
   906  					Ports: []GameServerPort{{
   907  						Name:          "main",
   908  						ContainerPort: 7777,
   909  						PortPolicy:    Dynamic,
   910  					}},
   911  					Container: "testing",
   912  					Template: corev1.PodTemplateSpec{
   913  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   914  							{
   915  								Name:  "testing",
   916  								Image: "testing/image",
   917  								Resources: corev1.ResourceRequirements{
   918  									Requests: corev1.ResourceList{
   919  										corev1.ResourceCPU:    resource.MustParse("30m"),
   920  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   921  									},
   922  									Limits: corev1.ResourceList{
   923  										corev1.ResourceCPU:    resource.MustParse("-30m"),
   924  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   925  									},
   926  								},
   927  							},
   928  						}},
   929  					},
   930  				},
   931  			},
   932  			applyDefaults: false,
   933  			want: field.ErrorList{
   934  				field.Invalid(
   935  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("cpu"),
   936  					"-30m",
   937  					"must be greater than or equal to 0",
   938  				),
   939  				field.Invalid(
   940  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
   941  					"30m",
   942  					"must be less than or equal to cpu limit of -30m",
   943  				),
   944  			},
   945  		},
   946  		{
   947  			description: "Memory Request > Limit",
   948  			gs: GameServer{
   949  				ObjectMeta: metav1.ObjectMeta{
   950  					Name:      "dev-game",
   951  					Namespace: "default",
   952  				},
   953  				Spec: GameServerSpec{
   954  					Ports: []GameServerPort{{
   955  						Name:          "main",
   956  						ContainerPort: 7777,
   957  						PortPolicy:    Dynamic,
   958  					}},
   959  					Container: "testing",
   960  					Template: corev1.PodTemplateSpec{
   961  						Spec: corev1.PodSpec{Containers: []corev1.Container{
   962  							{
   963  								Name:  "testing",
   964  								Image: "testing/image",
   965  								Resources: corev1.ResourceRequirements{
   966  									Requests: corev1.ResourceList{
   967  										corev1.ResourceCPU:    resource.MustParse("30m"),
   968  										corev1.ResourceMemory: resource.MustParse("55Mi"),
   969  									},
   970  									Limits: corev1.ResourceList{
   971  										corev1.ResourceCPU:    resource.MustParse("30m"),
   972  										corev1.ResourceMemory: resource.MustParse("32Mi"),
   973  									},
   974  								},
   975  							},
   976  						}},
   977  					},
   978  				},
   979  			},
   980  			applyDefaults: false,
   981  			want: field.ErrorList{
   982  				field.Invalid(
   983  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
   984  					"55Mi",
   985  					"must be less than or equal to memory limit of 32Mi",
   986  				),
   987  			},
   988  		},
   989  		{
   990  			description: "Memory negative request",
   991  			gs: GameServer{
   992  				ObjectMeta: metav1.ObjectMeta{
   993  					Name:      "dev-game",
   994  					Namespace: "default",
   995  				},
   996  				Spec: GameServerSpec{
   997  					Ports: []GameServerPort{{
   998  						Name:          "main",
   999  						ContainerPort: 7777,
  1000  						PortPolicy:    Dynamic,
  1001  					}},
  1002  					Container: "testing",
  1003  					Template: corev1.PodTemplateSpec{
  1004  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1005  							{
  1006  								Name:  "testing",
  1007  								Image: "testing/image",
  1008  								Resources: corev1.ResourceRequirements{
  1009  									Requests: corev1.ResourceList{
  1010  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1011  										corev1.ResourceMemory: resource.MustParse("-32Mi"),
  1012  									},
  1013  									Limits: corev1.ResourceList{
  1014  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1015  										corev1.ResourceMemory: resource.MustParse("32Mi"),
  1016  									},
  1017  								},
  1018  							},
  1019  						}},
  1020  					},
  1021  				},
  1022  			},
  1023  			applyDefaults: false,
  1024  			want: field.ErrorList{
  1025  				field.Invalid(
  1026  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("memory"),
  1027  					"-32Mi",
  1028  					"must be greater than or equal to 0",
  1029  				),
  1030  			},
  1031  		},
  1032  		{
  1033  			description: "Memory negative limit",
  1034  			gs: GameServer{
  1035  				ObjectMeta: metav1.ObjectMeta{
  1036  					Name:      "dev-game",
  1037  					Namespace: "default",
  1038  				},
  1039  				Spec: GameServerSpec{
  1040  					Ports: []GameServerPort{{
  1041  						Name:          "main",
  1042  						ContainerPort: 7777,
  1043  						PortPolicy:    Dynamic,
  1044  					}},
  1045  					Container: "testing",
  1046  					Template: corev1.PodTemplateSpec{
  1047  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1048  							{
  1049  								Name:  "testing",
  1050  								Image: "testing/image",
  1051  								Resources: corev1.ResourceRequirements{
  1052  									Requests: corev1.ResourceList{
  1053  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1054  										corev1.ResourceMemory: resource.MustParse("32Mi"),
  1055  									},
  1056  									Limits: corev1.ResourceList{
  1057  										corev1.ResourceCPU:    resource.MustParse("30m"),
  1058  										corev1.ResourceMemory: resource.MustParse("-32Mi"),
  1059  									},
  1060  								},
  1061  							},
  1062  						}},
  1063  					},
  1064  				},
  1065  			},
  1066  			applyDefaults: false,
  1067  			want: field.ErrorList{
  1068  				field.Invalid(
  1069  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("memory"),
  1070  					"-32Mi",
  1071  					"must be greater than or equal to 0",
  1072  				),
  1073  				field.Invalid(
  1074  					field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"),
  1075  					"32Mi",
  1076  					"must be less than or equal to memory limit of -32Mi",
  1077  				),
  1078  			},
  1079  		},
  1080  	}
  1081  
  1082  	for _, tc := range testCases {
  1083  		t.Run(tc.description, func(t *testing.T) {
  1084  			if tc.applyDefaults {
  1085  				tc.gs.ApplyDefaults()
  1086  			}
  1087  
  1088  			errs := tc.gs.Validate(fakeAPIHooks{})
  1089  			assert.ElementsMatch(t, tc.want, errs, "ErrorList check")
  1090  		})
  1091  	}
  1092  }
  1093  
  1094  func TestGameServerValidateFeatures(t *testing.T) {
  1095  	t.Parallel()
  1096  	runtime.FeatureTestMutex.Lock()
  1097  	defer runtime.FeatureTestMutex.Unlock()
  1098  
  1099  	portContainerName := "another-container"
  1100  
  1101  	testCases := []struct {
  1102  		description string
  1103  		feature     string
  1104  		gs          GameServer
  1105  		want        field.ErrorList
  1106  	}{
  1107  		{
  1108  			description: "Invalid container name, container was not found",
  1109  			gs: GameServer{
  1110  				ObjectMeta: metav1.ObjectMeta{
  1111  					Name:      "dev-game",
  1112  					Namespace: "default",
  1113  				},
  1114  				Spec: GameServerSpec{
  1115  					Ports: []GameServerPort{
  1116  						{
  1117  							Name:          "main",
  1118  							ContainerPort: 7777,
  1119  							PortPolicy:    Dynamic,
  1120  							Container:     &portContainerName,
  1121  						},
  1122  					},
  1123  					Container: "testing",
  1124  					Template: corev1.PodTemplateSpec{
  1125  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1126  							{Name: "testing", Image: "testing/image"},
  1127  						}},
  1128  					},
  1129  				},
  1130  			},
  1131  			want: field.ErrorList{
  1132  				field.Invalid(
  1133  					field.NewPath("spec", "ports").Index(0).Child("container"),
  1134  					"another-container",
  1135  					"Container must be empty or the name of a container in the pod template",
  1136  				),
  1137  			},
  1138  		},
  1139  		{
  1140  			description: "Multiple container ports, OK scenario",
  1141  			gs: GameServer{
  1142  				ObjectMeta: metav1.ObjectMeta{
  1143  					Name:      "dev-game",
  1144  					Namespace: "default",
  1145  				},
  1146  				Spec: GameServerSpec{
  1147  					Ports: []GameServerPort{
  1148  						{
  1149  							Name:          "main",
  1150  							ContainerPort: 7777,
  1151  							PortPolicy:    Dynamic,
  1152  						},
  1153  					},
  1154  					Container: "testing",
  1155  					Template: corev1.PodTemplateSpec{
  1156  						Spec: corev1.PodSpec{Containers: []corev1.Container{
  1157  							{Name: "testing", Image: "testing/image"},
  1158  						}},
  1159  					},
  1160  				},
  1161  			},
  1162  		},
  1163  		{
  1164  			description: "PlayerTracking is disabled, Players field specified",
  1165  			feature:     fmt.Sprintf("%s=false", runtime.FeaturePlayerTracking),
  1166  			gs: GameServer{
  1167  				Spec: GameServerSpec{
  1168  					Container: "testing",
  1169  					Players:   &PlayersSpec{InitialCapacity: 10},
  1170  					Template: corev1.PodTemplateSpec{
  1171  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1172  					},
  1173  				},
  1174  			},
  1175  			want: field.ErrorList{
  1176  				field.Forbidden(
  1177  					field.NewPath("spec", "players"),
  1178  					"Value cannot be set unless feature flag PlayerTracking is enabled",
  1179  				),
  1180  			},
  1181  		},
  1182  		{
  1183  			description: "PlayerTracking is enabled, Players field specified",
  1184  			feature:     fmt.Sprintf("%s=true", runtime.FeaturePlayerTracking),
  1185  			gs: GameServer{
  1186  				Spec: GameServerSpec{
  1187  					Container: "testing",
  1188  					Players:   &PlayersSpec{InitialCapacity: 10},
  1189  					Template: corev1.PodTemplateSpec{
  1190  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1191  					},
  1192  				},
  1193  			},
  1194  		},
  1195  		{
  1196  			description: "CountsAndLists is disabled, Counters field specified",
  1197  			feature:     fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists),
  1198  			gs: GameServer{
  1199  				Spec: GameServerSpec{
  1200  					Container: "testing",
  1201  					Counters:  map[string]CounterStatus{},
  1202  					Template: corev1.PodTemplateSpec{
  1203  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1204  					},
  1205  				},
  1206  			},
  1207  			want: field.ErrorList{
  1208  				field.Forbidden(
  1209  					field.NewPath("spec", "counters"),
  1210  					"Value cannot be set unless feature flag CountsAndLists is enabled",
  1211  				),
  1212  			},
  1213  		},
  1214  		{
  1215  			description: "CountsAndLists is disabled, Lists field specified",
  1216  			feature:     fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists),
  1217  			gs: GameServer{
  1218  				Spec: GameServerSpec{
  1219  					Container: "testing",
  1220  					Lists:     map[string]ListStatus{},
  1221  					Template: corev1.PodTemplateSpec{
  1222  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1223  					},
  1224  				},
  1225  			},
  1226  			want: field.ErrorList{
  1227  				field.Forbidden(
  1228  					field.NewPath("spec", "lists"),
  1229  					"Value cannot be set unless feature flag CountsAndLists is enabled",
  1230  				),
  1231  			},
  1232  		},
  1233  		{
  1234  			description: "CountsAndLists is enabled, Counters field specified",
  1235  			feature:     fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists),
  1236  			gs: GameServer{
  1237  				Spec: GameServerSpec{
  1238  					Container: "testing",
  1239  					Counters:  map[string]CounterStatus{},
  1240  					Template: corev1.PodTemplateSpec{
  1241  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1242  					},
  1243  				},
  1244  			},
  1245  		},
  1246  		{
  1247  			description: "CountsAndLists is enabled, Lists field specified",
  1248  			feature:     fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists),
  1249  			gs: GameServer{
  1250  				Spec: GameServerSpec{
  1251  					Container: "testing",
  1252  					Lists:     map[string]ListStatus{},
  1253  					Template: corev1.PodTemplateSpec{
  1254  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1255  					},
  1256  				},
  1257  			},
  1258  		},
  1259  		{
  1260  			description: "PortPolicyNone is disabled, PortPolicy field set to None",
  1261  			feature:     fmt.Sprintf("%s=false", runtime.FeaturePortPolicyNone),
  1262  			gs: GameServer{
  1263  				Spec: GameServerSpec{
  1264  					Ports: []GameServerPort{
  1265  						{
  1266  							Name:          "main",
  1267  							ContainerPort: 7777,
  1268  							PortPolicy:    None,
  1269  						},
  1270  					},
  1271  					Container: "testing",
  1272  					Lists:     map[string]ListStatus{},
  1273  					Template: corev1.PodTemplateSpec{
  1274  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1275  					},
  1276  				},
  1277  			},
  1278  			want: field.ErrorList{
  1279  				field.Forbidden(
  1280  					field.NewPath("spec.ports[0].portPolicy"),
  1281  					"Value cannot be set to None unless feature flag PortPolicyNone is enabled",
  1282  				),
  1283  			},
  1284  		},
  1285  		{
  1286  			description: "PortPolicyNone is enabled, PortPolicy field set to None",
  1287  			feature:     fmt.Sprintf("%s=true", runtime.FeaturePortPolicyNone),
  1288  			gs: GameServer{
  1289  				Spec: GameServerSpec{
  1290  					Ports: []GameServerPort{
  1291  						{
  1292  							Name:          "main",
  1293  							ContainerPort: 7777,
  1294  							PortPolicy:    None,
  1295  						},
  1296  					},
  1297  					Container: "testing",
  1298  					Lists:     map[string]ListStatus{},
  1299  					Template: corev1.PodTemplateSpec{
  1300  						Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}},
  1301  					},
  1302  				},
  1303  			},
  1304  		},
  1305  	}
  1306  
  1307  	for _, tc := range testCases {
  1308  		t.Run(tc.description, func(t *testing.T) {
  1309  			err := runtime.ParseFeatures(tc.feature)
  1310  			assert.NoError(t, err)
  1311  
  1312  			errs := tc.gs.Validate(fakeAPIHooks{})
  1313  			assert.ElementsMatch(t, tc.want, errs, "ErrorList check")
  1314  		})
  1315  	}
  1316  }
  1317  
  1318  func TestGameServerPodNoErrors(t *testing.T) {
  1319  	t.Parallel()
  1320  	fixture := defaultGameServer()
  1321  	fixture.ApplyDefaults()
  1322  
  1323  	pod, err := fixture.Pod(fakeAPIHooks{})
  1324  	assert.Nil(t, err, "Pod should not return an error")
  1325  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1326  	assert.Equal(t, fixture.ObjectMeta.Name, pod.Spec.Hostname)
  1327  	assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace)
  1328  	assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"])
  1329  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel])
  1330  	assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation])
  1331  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1332  	assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort)
  1333  	assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort)
  1334  	assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol)
  1335  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1336  }
  1337  
  1338  func TestGameServerPodHostName(t *testing.T) {
  1339  	t.Parallel()
  1340  
  1341  	fixture := defaultGameServer()
  1342  	fixture.ObjectMeta.Name = "test-1.0"
  1343  	fixture.ApplyDefaults()
  1344  	pod, err := fixture.Pod(fakeAPIHooks{})
  1345  	require.NoError(t, err)
  1346  	assert.Equal(t, "test-1-0", pod.Spec.Hostname)
  1347  
  1348  	fixture = defaultGameServer()
  1349  	fixture.ApplyDefaults()
  1350  	expected := "ORANGE"
  1351  	fixture.Spec.Template.Spec.Hostname = expected
  1352  	pod, err = fixture.Pod(fakeAPIHooks{})
  1353  	require.NoError(t, err)
  1354  	assert.Equal(t, expected, pod.Spec.Hostname)
  1355  }
  1356  
  1357  func TestGameServerPodContainerNotFoundErrReturned(t *testing.T) {
  1358  	t.Parallel()
  1359  
  1360  	containerName1 := "Container1"
  1361  	fixture := &GameServer{
  1362  		ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"},
  1363  		Spec: GameServerSpec{
  1364  			Container: "can-not-find-this-name",
  1365  			Ports: []GameServerPort{
  1366  				{
  1367  					Container:     &containerName1,
  1368  					ContainerPort: 7777,
  1369  					HostPort:      9999,
  1370  					PortPolicy:    Static,
  1371  				},
  1372  			},
  1373  			Template: corev1.PodTemplateSpec{
  1374  				Spec: corev1.PodSpec{
  1375  					Containers: []corev1.Container{{Name: "Container2", Image: "container/image"}},
  1376  				},
  1377  			},
  1378  		}, Status: GameServerStatus{State: GameServerStateCreating},
  1379  	}
  1380  
  1381  	_, err := fixture.Pod(fakeAPIHooks{})
  1382  	if assert.NotNil(t, err, "Pod should return an error") {
  1383  		assert.Equal(t, "failed to find container named Container1 in pod spec", err.Error())
  1384  	}
  1385  }
  1386  
  1387  func TestGameServerPodWithSidecarNoErrors(t *testing.T) {
  1388  	t.Parallel()
  1389  	runtime.FeatureTestMutex.Lock()
  1390  	defer runtime.FeatureTestMutex.Unlock()
  1391  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=false"))
  1392  
  1393  	fixture := defaultGameServer()
  1394  	fixture.ApplyDefaults()
  1395  
  1396  	sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"}
  1397  	fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk"
  1398  	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar)
  1399  	assert.Nil(t, err, "Pod should not return an error")
  1400  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1401  	assert.Len(t, pod.Spec.Containers, 2, "Should have two containers")
  1402  	assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName)
  1403  	assert.Equal(t, "sidecar", pod.Spec.Containers[0].Name)
  1404  	assert.Equal(t, "container", pod.Spec.Containers[1].Name)
  1405  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1406  }
  1407  
  1408  func TestGameServerPodWithInitSidecarNoErrors(t *testing.T) {
  1409  	t.Parallel()
  1410  	runtime.FeatureTestMutex.Lock()
  1411  	defer runtime.FeatureTestMutex.Unlock()
  1412  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true"))
  1413  
  1414  	fixture := defaultGameServer()
  1415  	fixture.ApplyDefaults()
  1416  
  1417  	sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"}
  1418  	fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk"
  1419  	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar)
  1420  	assert.Nil(t, err, "Pod should not return an error")
  1421  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1422  	assert.Len(t, pod.Spec.Containers, 1, "Should have one containers")
  1423  	assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName)
  1424  	assert.Equal(t, "sidecar", pod.Spec.InitContainers[0].Name)
  1425  	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy)
  1426  	assert.Equal(t, "container", pod.Spec.Containers[0].Name)
  1427  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1428  	assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever)
  1429  }
  1430  
  1431  func TestGameServerPodWithInitSidecarPrependsToExistingInitContainers(t *testing.T) {
  1432  	t.Parallel()
  1433  	runtime.FeatureTestMutex.Lock()
  1434  	defer runtime.FeatureTestMutex.Unlock()
  1435  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true"))
  1436  
  1437  	fixture := defaultGameServer()
  1438  	// Add existing init containers to the template
  1439  	existingInitContainer1 := corev1.Container{Name: "existing-init-1", Image: "existing/init1"}
  1440  	existingInitContainer2 := corev1.Container{Name: "existing-init-2", Image: "existing/init2"}
  1441  	fixture.Spec.Template.Spec.InitContainers = []corev1.Container{existingInitContainer1, existingInitContainer2}
  1442  	fixture.ApplyDefaults()
  1443  
  1444  	sidecar1 := corev1.Container{Name: "sidecar-1", Image: "container/sidecar1"}
  1445  	sidecar2 := corev1.Container{Name: "sidecar-2", Image: "container/sidecar2"}
  1446  	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar1, sidecar2)
  1447  	require.NoError(t, err)
  1448  
  1449  	// Verify that sidecars are placed at the beginning of InitContainers
  1450  	assert.Len(t, pod.Spec.InitContainers, 4, "Should have four init containers")
  1451  	assert.Equal(t, "sidecar-1", pod.Spec.InitContainers[0].Name, "First sidecar should be at index 0")
  1452  	assert.Equal(t, "sidecar-2", pod.Spec.InitContainers[1].Name, "Second sidecar should be at index 1")
  1453  	assert.Equal(t, "existing-init-1", pod.Spec.InitContainers[2].Name, "First existing init container should be at index 2")
  1454  	assert.Equal(t, "existing-init-2", pod.Spec.InitContainers[3].Name, "Second existing init container should be at index 3")
  1455  
  1456  	// Verify restart policies
  1457  	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy)
  1458  	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[1].RestartPolicy)
  1459  }
  1460  
  1461  func TestGameServerPodWithMultiplePortAllocations(t *testing.T) {
  1462  	fixture := defaultGameServer()
  1463  	containerName := "authContainer"
  1464  	fixture.Spec.Template.Spec.Containers = append(fixture.Spec.Template.Spec.Containers, corev1.Container{
  1465  		Name: containerName,
  1466  	})
  1467  	fixture.Spec.Ports = append(fixture.Spec.Ports, GameServerPort{
  1468  		Name:          "containerPort",
  1469  		PortPolicy:    Dynamic,
  1470  		Container:     &containerName,
  1471  		ContainerPort: 5000,
  1472  	})
  1473  	fixture.Spec.Container = fixture.Spec.Template.Spec.Containers[0].Name
  1474  	fixture.ApplyDefaults()
  1475  
  1476  	pod, err := fixture.Pod(fakeAPIHooks{})
  1477  	assert.NoError(t, err, "Pod should not return an error")
  1478  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
  1479  	assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace)
  1480  	assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"])
  1481  	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel])
  1482  	assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation])
  1483  	assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort)
  1484  	assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort)
  1485  	assert.Equal(t, *fixture.Spec.Ports[0].Container, pod.Spec.Containers[0].Name)
  1486  	assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol)
  1487  	assert.True(t, metav1.IsControlledBy(pod, fixture))
  1488  	assert.Equal(t, fixture.Spec.Ports[1].HostPort, pod.Spec.Containers[1].Ports[0].HostPort)
  1489  	assert.Equal(t, fixture.Spec.Ports[1].ContainerPort, pod.Spec.Containers[1].Ports[0].ContainerPort)
  1490  	assert.Equal(t, *fixture.Spec.Ports[1].Container, pod.Spec.Containers[1].Name)
  1491  	assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[1].Ports[0].Protocol)
  1492  }
  1493  
  1494  func TestGameServerPodObjectMeta(t *testing.T) {
  1495  	fixture := &GameServer{
  1496  		ObjectMeta: metav1.ObjectMeta{Name: "lucy"},
  1497  		Spec:       GameServerSpec{Container: "goat"},
  1498  	}
  1499  
  1500  	for desc, tc := range map[string]struct {
  1501  		scheduling apis.SchedulingStrategy
  1502  		wantSafe   string
  1503  	}{
  1504  		"packed": {
  1505  			scheduling: apis.Packed,
  1506  		},
  1507  		"distributed": {
  1508  			scheduling: apis.Distributed,
  1509  		},
  1510  	} {
  1511  		t.Run(desc, func(t *testing.T) {
  1512  			gs := fixture.DeepCopy()
  1513  			gs.Spec.Scheduling = tc.scheduling
  1514  			pod := &corev1.Pod{}
  1515  
  1516  			gs.podObjectMeta(pod)
  1517  
  1518  			assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Name)
  1519  			assert.Equal(t, gs.ObjectMeta.Namespace, pod.ObjectMeta.Namespace)
  1520  			assert.Equal(t, GameServerLabelRole, pod.ObjectMeta.Labels[RoleLabel])
  1521  			assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"])
  1522  			assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel])
  1523  			assert.Equal(t, "goat", pod.ObjectMeta.Annotations[GameServerContainerAnnotation])
  1524  			assert.True(t, metav1.IsControlledBy(pod, gs))
  1525  			assert.Equal(t, tc.wantSafe, pod.ObjectMeta.Annotations[PodSafeToEvictAnnotation])
  1526  		})
  1527  	}
  1528  }
  1529  
  1530  func TestGameServerPodScheduling(t *testing.T) {
  1531  	fixture := &corev1.Pod{Spec: corev1.PodSpec{}}
  1532  
  1533  	t.Run("packed", func(t *testing.T) {
  1534  		gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Packed}}
  1535  		pod := fixture.DeepCopy()
  1536  		gs.podScheduling(pod)
  1537  
  1538  		assert.Len(t, pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, 1)
  1539  		wpat := pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0]
  1540  		assert.Equal(t, int32(100), wpat.Weight)
  1541  		assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), GameServerLabelRole)
  1542  		assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), RoleLabel)
  1543  	})
  1544  
  1545  	t.Run("distributed", func(t *testing.T) {
  1546  		gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Distributed}}
  1547  		pod := fixture.DeepCopy()
  1548  		gs.podScheduling(pod)
  1549  		assert.Empty(t, pod.Spec.Affinity)
  1550  	})
  1551  }
  1552  
  1553  func TestGameServerDisableServiceAccount(t *testing.T) {
  1554  	t.Parallel()
  1555  
  1556  	gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{
  1557  		Template: corev1.PodTemplateSpec{
  1558  			Spec: corev1.PodSpec{
  1559  				Containers: []corev1.Container{{Name: "container", Image: "container/image"}},
  1560  			},
  1561  		},
  1562  	}}
  1563  
  1564  	gs.ApplyDefaults()
  1565  	pod, err := gs.Pod(fakeAPIHooks{})
  1566  	assert.NoError(t, err)
  1567  	assert.Len(t, pod.Spec.Containers, 1)
  1568  	assert.Empty(t, pod.Spec.Containers[0].VolumeMounts)
  1569  
  1570  	err = gs.DisableServiceAccount(pod)
  1571  	assert.NoError(t, err)
  1572  	assert.Len(t, pod.Spec.Containers, 1)
  1573  	assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1)
  1574  	assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath)
  1575  }
  1576  
  1577  func TestGameServerPassthroughPortAnnotation(t *testing.T) {
  1578  	t.Parallel()
  1579  	runtime.FeatureTestMutex.Lock()
  1580  	defer runtime.FeatureTestMutex.Unlock()
  1581  	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureAutopilotPassthroughPort)+"=true"))
  1582  	containerOne := "containerOne"
  1583  	containerTwo := "containerTwo"
  1584  	containerThree := "containerThree"
  1585  	containerFour := "containerFour"
  1586  	gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{
  1587  		Container: "containerOne",
  1588  		Ports: []GameServerPort{
  1589  			{Name: "defaultDynamicOne", PortPolicy: Dynamic, ContainerPort: 7659, Container: &containerOne},
  1590  			{Name: "defaultPassthroughOne", PortPolicy: Passthrough, Container: &containerOne},
  1591  			{Name: "defaultPassthroughTwo", PortPolicy: Passthrough, Container: &containerTwo},
  1592  			{Name: "defaultDynamicTwo", PortPolicy: Dynamic, ContainerPort: 7654, Container: &containerTwo},
  1593  			{Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7660, Container: &containerThree},
  1594  			{Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7661, Container: &containerThree},
  1595  			{Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7662, Container: &containerThree},
  1596  			{Name: "defaulPassthroughThree", PortPolicy: Passthrough, Container: &containerThree},
  1597  			{Name: "defaultPassthroughFour", PortPolicy: Passthrough, Container: &containerFour},
  1598  		},
  1599  		Template: corev1.PodTemplateSpec{
  1600  			Spec: corev1.PodSpec{
  1601  				Containers: []corev1.Container{
  1602  					{Name: "containerOne", Image: "container/image"},
  1603  					{Name: "containerTwo", Image: "container/image"},
  1604  					{Name: "containerThree", Image: "container/image"},
  1605  					{Name: "containerFour", Image: "container/image"},
  1606  				},
  1607  			},
  1608  		},
  1609  	}}
  1610  
  1611  	passthroughContainerPortMap := "{\"containerFour\":[0],\"containerOne\":[1],\"containerThree\":[3],\"containerTwo\":[0]}"
  1612  
  1613  	gs.ApplyDefaults()
  1614  	pod, err := gs.Pod(fakeAPIHooks{})
  1615  	assert.NoError(t, err)
  1616  	assert.Len(t, pod.Spec.Containers, 4)
  1617  	assert.Empty(t, pod.Spec.Containers[0].VolumeMounts)
  1618  	assert.Equal(t, pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation], passthroughContainerPortMap)
  1619  
  1620  	err = gs.DisableServiceAccount(pod)
  1621  	assert.NoError(t, err)
  1622  	assert.Len(t, pod.Spec.Containers, 4)
  1623  	assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1)
  1624  	assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath)
  1625  }
  1626  
  1627  func TestGameServerCountPorts(t *testing.T) {
  1628  	fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{
  1629  		{PortPolicy: Dynamic},
  1630  		{PortPolicy: Dynamic},
  1631  		{PortPolicy: Dynamic},
  1632  		{PortPolicy: Static},
  1633  	}}}
  1634  
  1635  	assert.Equal(t, 3, fixture.CountPorts(func(policy PortPolicy) bool {
  1636  		return policy == Dynamic
  1637  	}))
  1638  	assert.Equal(t, 1, fixture.CountPorts(func(policy PortPolicy) bool {
  1639  		return policy == Static
  1640  	}))
  1641  }
  1642  
  1643  func TestGameServerCountPortsForRange(t *testing.T) {
  1644  	fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{
  1645  		{PortPolicy: Dynamic, Range: "test"},
  1646  		{PortPolicy: Dynamic},
  1647  		{PortPolicy: Dynamic, Range: "test"},
  1648  		{PortPolicy: Static, Range: "test"},
  1649  	}}}
  1650  
  1651  	assert.Equal(t, 2, fixture.CountPortsForRange("test", func(policy PortPolicy) bool {
  1652  		return policy == Dynamic
  1653  	}))
  1654  	assert.Equal(t, 1, fixture.CountPortsForRange("test", func(policy PortPolicy) bool {
  1655  		return policy == Static
  1656  	}))
  1657  }
  1658  
  1659  func TestGameServerPatch(t *testing.T) {
  1660  	fixture := &GameServer{
  1661  		ObjectMeta: metav1.ObjectMeta{Name: "lucy", ResourceVersion: "1234"},
  1662  		Spec:       GameServerSpec{Container: "goat"},
  1663  	}
  1664  
  1665  	delta := fixture.DeepCopy()
  1666  	delta.Spec.Container = "bear"
  1667  
  1668  	patch, err := fixture.Patch(delta)
  1669  	assert.Nil(t, err)
  1670  
  1671  	assert.Contains(t, string(patch), `{"op":"replace","path":"/spec/container","value":"bear"}`)
  1672  	assert.Contains(t, string(patch), `{"op":"test","path":"/metadata/resourceVersion","value":"1234"}`)
  1673  }
  1674  
  1675  func TestGameServerGetDevAddress(t *testing.T) {
  1676  	devGs := &GameServer{
  1677  		ObjectMeta: metav1.ObjectMeta{
  1678  			Name:        "dev-game",
  1679  			Namespace:   "default",
  1680  			Annotations: map[string]string{DevAddressAnnotation: ipFixture},
  1681  		},
  1682  		Spec: GameServerSpec{
  1683  			Ports: []GameServerPort{{HostPort: 7777, PortPolicy: Static}},
  1684  			Template: corev1.PodTemplateSpec{
  1685  				Spec: corev1.PodSpec{
  1686  					Containers: []corev1.Container{{Name: "container", Image: "container/image"}},
  1687  				},
  1688  			},
  1689  		},
  1690  	}
  1691  
  1692  	devAddress, isDev := devGs.GetDevAddress()
  1693  	assert.True(t, isDev, "dev-game should had a dev-address")
  1694  	assert.Equal(t, ipFixture, devAddress, "dev-address IP address should be 127.1.1.1")
  1695  
  1696  	regularGs := devGs.DeepCopy()
  1697  	regularGs.ObjectMeta.Annotations = map[string]string{}
  1698  	devAddress, isDev = regularGs.GetDevAddress()
  1699  	assert.False(t, isDev, "dev-game should NOT have a dev-address")
  1700  	assert.Equal(t, "", devAddress, "dev-address IP address should be 127.1.1.1")
  1701  }
  1702  
  1703  func TestGameServerIsDeletable(t *testing.T) {
  1704  	gs := &GameServer{Status: GameServerStatus{State: GameServerStateStarting}}
  1705  	assert.True(t, gs.IsDeletable())
  1706  
  1707  	gs.Status.State = GameServerStateAllocated
  1708  	assert.False(t, gs.IsDeletable())
  1709  
  1710  	gs.Status.State = GameServerStateReserved
  1711  	assert.False(t, gs.IsDeletable())
  1712  
  1713  	now := metav1.Now()
  1714  	gs.ObjectMeta.DeletionTimestamp = &now
  1715  	assert.True(t, gs.IsDeletable())
  1716  
  1717  	gs.Status.State = GameServerStateAllocated
  1718  	assert.True(t, gs.IsDeletable())
  1719  
  1720  	gs.Status.State = GameServerStateReady
  1721  	assert.True(t, gs.IsDeletable())
  1722  }
  1723  
  1724  func TestGameServerIsBeforeReady(t *testing.T) {
  1725  	fixtures := []struct {
  1726  		state    GameServerState
  1727  		expected bool
  1728  	}{
  1729  		{GameServerStatePortAllocation, true},
  1730  		{GameServerStateCreating, true},
  1731  		{GameServerStateStarting, true},
  1732  		{GameServerStateScheduled, true},
  1733  		{GameServerStateRequestReady, true},
  1734  		{GameServerStateReady, false},
  1735  		{GameServerStateShutdown, false},
  1736  		{GameServerStateError, false},
  1737  		{GameServerStateUnhealthy, false},
  1738  		{GameServerStateReserved, false},
  1739  		{GameServerStateAllocated, false},
  1740  	}
  1741  
  1742  	for _, test := range fixtures {
  1743  		t.Run(string(test.state), func(t *testing.T) {
  1744  			gs := &GameServer{Status: GameServerStatus{State: test.state}}
  1745  			assert.Equal(t, test.expected, gs.IsBeforeReady(), test.state)
  1746  		})
  1747  	}
  1748  }
  1749  
  1750  func TestGameServerApplyToPodContainer(t *testing.T) {
  1751  	t.Parallel()
  1752  	type expected struct {
  1753  		err string
  1754  		tty bool
  1755  	}
  1756  
  1757  	testCases := []struct {
  1758  		description string
  1759  		gs          *GameServer
  1760  		expected    expected
  1761  	}{
  1762  		{
  1763  			description: "OK, no error",
  1764  			gs: &GameServer{
  1765  				Spec: GameServerSpec{
  1766  					Container: "mycontainer",
  1767  					Template: corev1.PodTemplateSpec{
  1768  						Spec: corev1.PodSpec{
  1769  							Containers: []corev1.Container{
  1770  								{Name: "mycontainer", Image: "foo/mycontainer"},
  1771  								{Name: "notmycontainer", Image: "foo/notmycontainer"},
  1772  							},
  1773  						},
  1774  					},
  1775  				},
  1776  			},
  1777  			expected: expected{
  1778  				err: "",
  1779  				tty: true,
  1780  			},
  1781  		},
  1782  		{
  1783  			description: "container not found, error is returned",
  1784  			gs: &GameServer{
  1785  				Spec: GameServerSpec{
  1786  					Container: "mycontainer-WRONG-NAME",
  1787  					Template: corev1.PodTemplateSpec{
  1788  						Spec: corev1.PodSpec{
  1789  							Containers: []corev1.Container{
  1790  								{Name: "mycontainer", Image: "foo/mycontainer"},
  1791  								{Name: "notmycontainer", Image: "foo/notmycontainer"},
  1792  							},
  1793  						},
  1794  					},
  1795  				},
  1796  			},
  1797  			expected: expected{
  1798  				err: "failed to find container named mycontainer-WRONG-NAME in pod spec",
  1799  				tty: false,
  1800  			},
  1801  		},
  1802  	}
  1803  
  1804  	for _, tc := range testCases {
  1805  		t.Run(tc.description, func(t *testing.T) {
  1806  			pod := &corev1.Pod{Spec: *tc.gs.Spec.Template.Spec.DeepCopy()}
  1807  			result := tc.gs.ApplyToPodContainer(pod, tc.gs.Spec.Container, func(c corev1.Container) corev1.Container {
  1808  				//  easy thing to change and test for
  1809  				c.TTY = true
  1810  				return c
  1811  			})
  1812  
  1813  			if tc.expected.err != "" && assert.NotNil(t, result) {
  1814  				assert.Equal(t, tc.expected.err, result.Error())
  1815  			}
  1816  			assert.Equal(t, tc.expected.tty, pod.Spec.Containers[0].TTY)
  1817  			assert.False(t, pod.Spec.Containers[1].TTY)
  1818  		})
  1819  	}
  1820  }
  1821  
  1822  func defaultGameServer() *GameServer {
  1823  	return &GameServer{
  1824  		ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"},
  1825  		Spec: GameServerSpec{
  1826  			Ports: []GameServerPort{
  1827  				{
  1828  					ContainerPort: 7777,
  1829  					HostPort:      9999,
  1830  					PortPolicy:    Static,
  1831  				},
  1832  			},
  1833  			Template: corev1.PodTemplateSpec{
  1834  				Spec: corev1.PodSpec{
  1835  					Containers: []corev1.Container{{Name: "container", Image: "container/image"}},
  1836  				},
  1837  			},
  1838  		}, Status: GameServerStatus{State: GameServerStateCreating},
  1839  	}
  1840  }
  1841  
  1842  func TestGameServerUpdateCount(t *testing.T) {
  1843  	t.Parallel()
  1844  
  1845  	testCases := map[string]struct {
  1846  		gs      GameServer
  1847  		name    string
  1848  		action  string
  1849  		amount  int64
  1850  		want    CounterStatus
  1851  		wantErr bool
  1852  	}{
  1853  		"counter not in game server no-op and error": {
  1854  			gs: GameServer{Status: GameServerStatus{
  1855  				Counters: map[string]CounterStatus{
  1856  					"foos": {
  1857  						Count:    0,
  1858  						Capacity: 100,
  1859  					},
  1860  				},
  1861  			}},
  1862  			name:    "foo",
  1863  			action:  "Increment",
  1864  			amount:  1,
  1865  			wantErr: true,
  1866  		},
  1867  		"negative amount no-op and error": {
  1868  			gs: GameServer{Status: GameServerStatus{
  1869  				Counters: map[string]CounterStatus{
  1870  					"foos": {
  1871  						Count:    1,
  1872  						Capacity: 100,
  1873  					},
  1874  				},
  1875  			}},
  1876  			name:   "foos",
  1877  			action: "Decrement",
  1878  			amount: -1,
  1879  			want: CounterStatus{
  1880  				Count:    1,
  1881  				Capacity: 100,
  1882  			},
  1883  			wantErr: true,
  1884  		},
  1885  		"increment by 1": {
  1886  			gs: GameServer{Status: GameServerStatus{
  1887  				Counters: map[string]CounterStatus{
  1888  					"players": {
  1889  						Count:    0,
  1890  						Capacity: 100,
  1891  					},
  1892  				},
  1893  			}},
  1894  			name:   "players",
  1895  			action: "Increment",
  1896  			amount: 1,
  1897  			want: CounterStatus{
  1898  				Count:    1,
  1899  				Capacity: 100,
  1900  			},
  1901  			wantErr: false,
  1902  		},
  1903  		"decrement by 10": {
  1904  			gs: GameServer{Status: GameServerStatus{
  1905  				Counters: map[string]CounterStatus{
  1906  					"bars": {
  1907  						Count:    99,
  1908  						Capacity: 100,
  1909  					},
  1910  				},
  1911  			}},
  1912  			name:   "bars",
  1913  			action: "Decrement",
  1914  			amount: 10,
  1915  			want: CounterStatus{
  1916  				Count:    89,
  1917  				Capacity: 100,
  1918  			},
  1919  			wantErr: false,
  1920  		},
  1921  		"incorrect action no-op and error": {
  1922  			gs: GameServer{Status: GameServerStatus{
  1923  				Counters: map[string]CounterStatus{
  1924  					"bazes": {
  1925  						Count:    99,
  1926  						Capacity: 100,
  1927  					},
  1928  				},
  1929  			}},
  1930  			name:   "bazes",
  1931  			action: "decrement",
  1932  			amount: 10,
  1933  			want: CounterStatus{
  1934  				Count:    99,
  1935  				Capacity: 100,
  1936  			},
  1937  			wantErr: true,
  1938  		},
  1939  		"decrement beyond zero truncated": {
  1940  			gs: GameServer{Status: GameServerStatus{
  1941  				Counters: map[string]CounterStatus{
  1942  					"baz": {
  1943  						Count:    99,
  1944  						Capacity: 100,
  1945  					},
  1946  				},
  1947  			}},
  1948  			name:   "baz",
  1949  			action: "Decrement",
  1950  			amount: 100,
  1951  			want: CounterStatus{
  1952  				Count:    0,
  1953  				Capacity: 100,
  1954  			},
  1955  			wantErr: false,
  1956  		},
  1957  		"increment beyond capacity truncated": {
  1958  			gs: GameServer{Status: GameServerStatus{
  1959  				Counters: map[string]CounterStatus{
  1960  					"splayers": {
  1961  						Count:    99,
  1962  						Capacity: 100,
  1963  					},
  1964  				},
  1965  			}},
  1966  			name:   "splayers",
  1967  			action: "Increment",
  1968  			amount: 2,
  1969  			want: CounterStatus{
  1970  				Count:    100,
  1971  				Capacity: 100,
  1972  			},
  1973  			wantErr: false,
  1974  		},
  1975  	}
  1976  
  1977  	for test, testCase := range testCases {
  1978  		t.Run(test, func(t *testing.T) {
  1979  			err := testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount)
  1980  			if err != nil {
  1981  				assert.True(t, testCase.wantErr)
  1982  			} else {
  1983  				assert.False(t, testCase.wantErr)
  1984  			}
  1985  			if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
  1986  				assert.Equal(t, testCase.want, counter)
  1987  			}
  1988  		})
  1989  	}
  1990  }
  1991  
  1992  func TestGameServerUpdateCounterCapacity(t *testing.T) {
  1993  	t.Parallel()
  1994  
  1995  	testCases := map[string]struct {
  1996  		gs       GameServer
  1997  		name     string
  1998  		capacity int64
  1999  		want     CounterStatus
  2000  		wantErr  bool
  2001  	}{
  2002  		"counter not in game server no-op with error": {
  2003  			gs: GameServer{Status: GameServerStatus{
  2004  				Counters: map[string]CounterStatus{
  2005  					"foos": {
  2006  						Count:    0,
  2007  						Capacity: 100,
  2008  					},
  2009  				},
  2010  			}},
  2011  			name:     "foo",
  2012  			capacity: 1000,
  2013  			wantErr:  true,
  2014  		},
  2015  		"capacity less than zero no-op with error": {
  2016  			gs: GameServer{Status: GameServerStatus{
  2017  				Counters: map[string]CounterStatus{
  2018  					"foos": {
  2019  						Count:    0,
  2020  						Capacity: 100,
  2021  					},
  2022  				},
  2023  			}},
  2024  			name:     "foos",
  2025  			capacity: -1000,
  2026  			want: CounterStatus{
  2027  				Count:    0,
  2028  				Capacity: 100,
  2029  			},
  2030  			wantErr: true,
  2031  		},
  2032  		"update capacity": {
  2033  			gs: GameServer{Status: GameServerStatus{
  2034  				Counters: map[string]CounterStatus{
  2035  					"sessions": {
  2036  						Count:    0,
  2037  						Capacity: 100,
  2038  					},
  2039  				},
  2040  			}},
  2041  			name:     "sessions",
  2042  			capacity: 9223372036854775807,
  2043  			want: CounterStatus{
  2044  				Count:    0,
  2045  				Capacity: 9223372036854775807,
  2046  			},
  2047  		},
  2048  	}
  2049  
  2050  	for test, testCase := range testCases {
  2051  		t.Run(test, func(t *testing.T) {
  2052  			err := testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity)
  2053  			if err != nil {
  2054  				assert.True(t, testCase.wantErr)
  2055  			} else {
  2056  				assert.False(t, testCase.wantErr)
  2057  			}
  2058  			if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok {
  2059  				assert.Equal(t, testCase.want, counter)
  2060  			}
  2061  		})
  2062  	}
  2063  }
  2064  
  2065  func TestGameServerUpdateListCapacity(t *testing.T) {
  2066  	t.Parallel()
  2067  
  2068  	testCases := map[string]struct {
  2069  		gs       GameServer
  2070  		name     string
  2071  		capacity int64
  2072  		want     ListStatus
  2073  		wantErr  bool
  2074  	}{
  2075  		"list not in game server no-op with error": {
  2076  			gs: GameServer{Status: GameServerStatus{
  2077  				Lists: map[string]ListStatus{
  2078  					"things": {
  2079  						Values:   []string{},
  2080  						Capacity: 100,
  2081  					},
  2082  				},
  2083  			}},
  2084  			name:     "thing",
  2085  			capacity: 1000,
  2086  			wantErr:  true,
  2087  		},
  2088  		"update list capacity": {
  2089  			gs: GameServer{Status: GameServerStatus{
  2090  				Lists: map[string]ListStatus{
  2091  					"things": {
  2092  						Values:   []string{},
  2093  						Capacity: 100,
  2094  					},
  2095  				},
  2096  			}},
  2097  			name:     "things",
  2098  			capacity: 1000,
  2099  			want: ListStatus{
  2100  				Values:   []string{},
  2101  				Capacity: 1000,
  2102  			},
  2103  			wantErr: false,
  2104  		},
  2105  		"list capacity above max no-op with error": {
  2106  			gs: GameServer{Status: GameServerStatus{
  2107  				Lists: map[string]ListStatus{
  2108  					"slings": {
  2109  						Values:   []string{},
  2110  						Capacity: 100,
  2111  					},
  2112  				},
  2113  			}},
  2114  			name:     "slings",
  2115  			capacity: 10000,
  2116  			want: ListStatus{
  2117  				Values:   []string{},
  2118  				Capacity: 100,
  2119  			},
  2120  			wantErr: true,
  2121  		},
  2122  		"list capacity less than zero no-op with error": {
  2123  			gs: GameServer{Status: GameServerStatus{
  2124  				Lists: map[string]ListStatus{
  2125  					"flings": {
  2126  						Values:   []string{},
  2127  						Capacity: 999,
  2128  					},
  2129  				},
  2130  			}},
  2131  			name:     "flings",
  2132  			capacity: -100,
  2133  			want: ListStatus{
  2134  				Values:   []string{},
  2135  				Capacity: 999,
  2136  			},
  2137  			wantErr: true,
  2138  		},
  2139  	}
  2140  
  2141  	for test, testCase := range testCases {
  2142  		t.Run(test, func(t *testing.T) {
  2143  			err := testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity)
  2144  			if err != nil {
  2145  				assert.True(t, testCase.wantErr)
  2146  			} else {
  2147  				assert.False(t, testCase.wantErr)
  2148  			}
  2149  			if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
  2150  				assert.Equal(t, testCase.want, list)
  2151  			}
  2152  		})
  2153  	}
  2154  }
  2155  
  2156  func TestGameServerAppendListValues(t *testing.T) {
  2157  	t.Parallel()
  2158  
  2159  	var nilSlice []string
  2160  
  2161  	testCases := map[string]struct {
  2162  		gs      GameServer
  2163  		name    string
  2164  		values  []string
  2165  		want    ListStatus
  2166  		wantErr bool
  2167  	}{
  2168  		"list not in game server no-op with error": {
  2169  			gs: GameServer{Status: GameServerStatus{
  2170  				Lists: map[string]ListStatus{
  2171  					"things": {
  2172  						Values:   []string{},
  2173  						Capacity: 100,
  2174  					},
  2175  				},
  2176  			}},
  2177  			name:    "thing",
  2178  			values:  []string{"thing1", "thing2", "thing3"},
  2179  			wantErr: true,
  2180  		},
  2181  		"append values": {
  2182  			gs: GameServer{Status: GameServerStatus{
  2183  				Lists: map[string]ListStatus{
  2184  					"things": {
  2185  						Values:   []string{"thing1"},
  2186  						Capacity: 100,
  2187  					},
  2188  				},
  2189  			}},
  2190  			name:   "things",
  2191  			values: []string{"thing2", "thing3"},
  2192  			want: ListStatus{
  2193  				Values:   []string{"thing1", "thing2", "thing3"},
  2194  				Capacity: 100,
  2195  			},
  2196  			wantErr: false,
  2197  		},
  2198  		"append values with silent drop of duplicates": {
  2199  			gs: GameServer{Status: GameServerStatus{
  2200  				Lists: map[string]ListStatus{
  2201  					"games": {
  2202  						Values:   []string{"game0"},
  2203  						Capacity: 10,
  2204  					},
  2205  				},
  2206  			}},
  2207  			name:   "games",
  2208  			values: []string{"game1", "game2", "game2", "game1"},
  2209  			want: ListStatus{
  2210  				Values:   []string{"game0", "game1", "game2"},
  2211  				Capacity: 10,
  2212  			},
  2213  			wantErr: false,
  2214  		},
  2215  		"append values with silent drop of duplicates in original list": {
  2216  			gs: GameServer{Status: GameServerStatus{
  2217  				Lists: map[string]ListStatus{
  2218  					"objects": {
  2219  						Values:   []string{"object1", "object2"},
  2220  						Capacity: 10,
  2221  					},
  2222  				},
  2223  			}},
  2224  			name:   "objects",
  2225  			values: []string{"object2", "object1", "object3", "object3"},
  2226  			want: ListStatus{
  2227  				Values:   []string{"object1", "object2", "object3"},
  2228  				Capacity: 10,
  2229  			},
  2230  			wantErr: false,
  2231  		},
  2232  		"append nil values": {
  2233  			gs: GameServer{Status: GameServerStatus{
  2234  				Lists: map[string]ListStatus{
  2235  					"blings": {
  2236  						Values:   []string{"bling1"},
  2237  						Capacity: 10,
  2238  					},
  2239  				},
  2240  			}},
  2241  			name:   "blings",
  2242  			values: nilSlice,
  2243  			want: ListStatus{
  2244  				Values:   []string{"bling1"},
  2245  				Capacity: 10,
  2246  			},
  2247  			wantErr: true,
  2248  		},
  2249  		"append too many values truncates list": {
  2250  			gs: GameServer{Status: GameServerStatus{
  2251  				Lists: map[string]ListStatus{
  2252  					"bananaslugs": {
  2253  						Values:   []string{"bananaslugs1", "bananaslug2", "bananaslug3"},
  2254  						Capacity: 5,
  2255  					},
  2256  				},
  2257  			}},
  2258  			name:   "bananaslugs",
  2259  			values: []string{"bananaslug4", "bananaslug5", "bananaslug6"},
  2260  			want: ListStatus{
  2261  				Values:   []string{"bananaslugs1", "bananaslug2", "bananaslug3", "bananaslug4", "bananaslug5"},
  2262  				Capacity: 5,
  2263  			},
  2264  			wantErr: false,
  2265  		},
  2266  	}
  2267  
  2268  	for test, testCase := range testCases {
  2269  		t.Run(test, func(t *testing.T) {
  2270  			err := testCase.gs.AppendListValues(testCase.name, testCase.values)
  2271  			if err != nil {
  2272  				assert.True(t, testCase.wantErr)
  2273  			} else {
  2274  				assert.False(t, testCase.wantErr)
  2275  			}
  2276  			if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
  2277  				assert.Equal(t, testCase.want, list)
  2278  			}
  2279  		})
  2280  	}
  2281  }
  2282  
  2283  func TestGameServerDeleteListValues(t *testing.T) {
  2284  	t.Parallel()
  2285  
  2286  	testCases := map[string]struct {
  2287  		gs      GameServer
  2288  		name    string
  2289  		want    ListStatus
  2290  		values  []string
  2291  		wantErr bool
  2292  	}{
  2293  		"list not in game server no-op and error": {
  2294  			gs: GameServer{Status: GameServerStatus{
  2295  				Lists: map[string]ListStatus{
  2296  					"foos": {
  2297  						Values:   []string{"foo", "bar", "bax"},
  2298  						Capacity: 100,
  2299  					},
  2300  				},
  2301  			}},
  2302  			name:    "foo",
  2303  			values:  []string{"bar", "baz"},
  2304  			wantErr: true,
  2305  		},
  2306  		"delete list value - one value not present": {
  2307  			gs: GameServer{Status: GameServerStatus{
  2308  				Lists: map[string]ListStatus{
  2309  					"foo": {
  2310  						Values:   []string{"foo", "bar", "bax"},
  2311  						Capacity: 100,
  2312  					},
  2313  				},
  2314  			}},
  2315  			name:    "foo",
  2316  			values:  []string{"bar", "baz"},
  2317  			wantErr: false,
  2318  			want: ListStatus{
  2319  				Values:   []string{"foo", "bax"},
  2320  				Capacity: 100,
  2321  			},
  2322  		},
  2323  	}
  2324  
  2325  	for test, testCase := range testCases {
  2326  		t.Run(test, func(t *testing.T) {
  2327  			err := testCase.gs.DeleteListValues(testCase.name, testCase.values)
  2328  			if err != nil {
  2329  				assert.True(t, testCase.wantErr)
  2330  			} else {
  2331  				assert.False(t, testCase.wantErr)
  2332  			}
  2333  			if list, ok := testCase.gs.Status.Lists[testCase.name]; ok {
  2334  				assert.Equal(t, testCase.want, list)
  2335  			}
  2336  		})
  2337  	}
  2338  }
  2339  
  2340  func TestMergeRemoveDuplicates(t *testing.T) {
  2341  	t.Parallel()
  2342  
  2343  	testCases := map[string]struct {
  2344  		str1 []string
  2345  		str2 []string
  2346  		want []string
  2347  	}{
  2348  		"empty string arrays": {
  2349  			str1: []string{},
  2350  			str2: []string{},
  2351  			want: []string{},
  2352  		},
  2353  		"no duplicates": {
  2354  			str1: []string{"one"},
  2355  			str2: []string{"two", "three"},
  2356  			want: []string{"one", "two", "three"},
  2357  		},
  2358  		"remove one duplicate": {
  2359  			str1: []string{"one", "one", "one"},
  2360  			str2: []string{"one", "one", "one"},
  2361  			want: []string{"one"},
  2362  		},
  2363  		"remove multiple duplicates": {
  2364  			str1: []string{"one", "two"},
  2365  			str2: []string{"two", "one"},
  2366  			want: []string{"one", "two"},
  2367  		},
  2368  	}
  2369  
  2370  	for test, testCase := range testCases {
  2371  		t.Run(test, func(t *testing.T) {
  2372  			got := MergeRemoveDuplicates(testCase.str1, testCase.str2)
  2373  			assert.Equal(t, testCase.want, got)
  2374  		})
  2375  	}
  2376  }