agones.dev/agones@v1.53.0/pkg/apis/autoscaling/v1/fleetautoscaler_test.go (about)

     1  // Copyright 2018 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  	"testing"
    19  	"time"
    20  
    21  	"agones.dev/agones/pkg/util/runtime"
    22  	"github.com/stretchr/testify/assert"
    23  	admregv1 "k8s.io/api/admissionregistration/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/util/intstr"
    26  )
    27  
    28  func TestFleetAutoscalerValidateUpdate(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	t.Run("bad buffer size", func(t *testing.T) {
    32  		fas := defaultFixture()
    33  		fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(0)
    34  		causes := fas.Validate()
    35  
    36  		assert.Len(t, causes, 1)
    37  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    38  	})
    39  
    40  	t.Run("bad min replicas", func(t *testing.T) {
    41  		fas := defaultFixture()
    42  		fas.Spec.Policy.Buffer.MinReplicas = 2
    43  
    44  		causes := fas.Validate()
    45  
    46  		assert.Len(t, causes, 1)
    47  		assert.Equal(t, "spec.policy.buffer.minReplicas", causes[0].Field)
    48  	})
    49  
    50  	t.Run("bad max replicas", func(t *testing.T) {
    51  		fas := defaultFixture()
    52  		fas.Spec.Policy.Buffer.MaxReplicas = 2
    53  		causes := fas.Validate()
    54  
    55  		assert.Len(t, causes, 1)
    56  		assert.Equal(t, "spec.policy.buffer.maxReplicas", causes[0].Field)
    57  	})
    58  
    59  	t.Run("minReplicas > maxReplicas", func(t *testing.T) {
    60  		fas := defaultFixture()
    61  		fas.Spec.Policy.Buffer.MinReplicas = 20
    62  		causes := fas.Validate()
    63  
    64  		assert.Len(t, causes, 1)
    65  		assert.Equal(t, "spec.policy.buffer.minReplicas", causes[0].Field)
    66  	})
    67  
    68  	t.Run("bufferSize good percent", func(t *testing.T) {
    69  		fas := defaultFixture()
    70  		fas.Spec.Policy.Buffer.MinReplicas = 1
    71  		fas.Spec.Policy.Buffer.BufferSize = intstr.FromString("20%")
    72  		causes := fas.Validate()
    73  
    74  		assert.Len(t, causes, 0)
    75  	})
    76  
    77  	t.Run("bufferSize bad percent", func(t *testing.T) {
    78  		fas := defaultFixture()
    79  		fas.Spec.Policy.Buffer.MinReplicas = 1
    80  
    81  		fasCopy := fas.DeepCopy()
    82  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("120%")
    83  		causes := fasCopy.Validate()
    84  		assert.Len(t, causes, 1)
    85  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    86  
    87  		fasCopy = fas.DeepCopy()
    88  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("0%")
    89  		causes = fasCopy.Validate()
    90  		assert.Len(t, causes, 1)
    91  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    92  
    93  		fasCopy = fas.DeepCopy()
    94  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("-10%")
    95  		causes = fasCopy.Validate()
    96  		assert.Len(t, causes, 1)
    97  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    98  		fasCopy = fas.DeepCopy()
    99  
   100  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("notgood")
   101  		causes = fasCopy.Validate()
   102  		assert.Len(t, causes, 1)
   103  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
   104  	})
   105  
   106  	t.Run("bad min replicas with percentage value of bufferSize", func(t *testing.T) {
   107  		fas := defaultFixture()
   108  		fas.Spec.Policy.Buffer.BufferSize = intstr.FromString("10%")
   109  		fas.Spec.Policy.Buffer.MinReplicas = 0
   110  
   111  		causes := fas.Validate()
   112  
   113  		assert.Len(t, causes, 1)
   114  		assert.Equal(t, "spec.policy.buffer.minReplicas", causes[0].Field)
   115  	})
   116  
   117  	t.Run("bad sync interval seconds", func(t *testing.T) {
   118  
   119  		fas := defaultFixture()
   120  		fas.Spec.Sync.FixedInterval.Seconds = 0
   121  
   122  		causes := fas.Validate()
   123  
   124  		assert.Len(t, causes, 1)
   125  		assert.Equal(t, "spec.sync.fixedInterval.seconds", causes[0].Field)
   126  	})
   127  }
   128  func TestFleetAutoscalerWebhookValidateUpdate(t *testing.T) {
   129  	t.Parallel()
   130  
   131  	t.Run("good service value", func(t *testing.T) {
   132  		fas := webhookFixture()
   133  		causes := fas.Validate()
   134  
   135  		assert.Len(t, causes, 0)
   136  	})
   137  
   138  	t.Run("good url value", func(t *testing.T) {
   139  		fas := webhookFixture()
   140  		url := "http://good.example.com"
   141  		fas.Spec.Policy.Webhook.URL = &url
   142  		fas.Spec.Policy.Webhook.Service = nil
   143  		causes := fas.Validate()
   144  
   145  		assert.Len(t, causes, 0)
   146  	})
   147  
   148  	t.Run("bad URL and service value", func(t *testing.T) {
   149  		fas := webhookFixture()
   150  		fas.Spec.Policy.Webhook.URL = nil
   151  		fas.Spec.Policy.Webhook.Service = nil
   152  		causes := fas.Validate()
   153  
   154  		assert.Len(t, causes, 1)
   155  		assert.Equal(t, "spec.policy.webhook", causes[0].Field)
   156  	})
   157  
   158  	t.Run("both URL and service value are used - fail", func(t *testing.T) {
   159  
   160  		fas := webhookFixture()
   161  		url := "123"
   162  		fas.Spec.Policy.Webhook.URL = &url
   163  
   164  		causes := fas.Validate()
   165  
   166  		assert.Len(t, causes, 1)
   167  		assert.Equal(t, "spec.policy.webhook.url", causes[0].Field)
   168  	})
   169  
   170  	goodCaBundle := "\n-----BEGIN CERTIFICATE-----\nMIIDXjCCAkYCCQDvT9MAXwnuqDANBgkqhkiG9w0BAQsFADBxMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEP\nMA0GA1UECgwGQWdvbmVzMQ8wDQYDVQQLDAZBZ29uZXMxEzARBgNVBAMMCmFnb25l\ncy5kZXYwHhcNMTkwMTAzMTEwNTA0WhcNMjExMDIzMTEwNTA0WjBxMQswCQYDVQQG\nEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll\ndzEPMA0GA1UECgwGQWdvbmVzMQ8wDQYDVQQLDAZBZ29uZXMxEzARBgNVBAMMCmFn\nb25lcy5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFwohJp3xK\n4iORkJXNO2KEkdrVYK7xpXTrPvZqLzoyMBOXi9b+lOKILUPaKtZ33GIwola31bHp\ni7V97vh3irIQVpap6uncesRTX0qk5Y70f7T6lByMKDsxi5ddea3ztAftH+PMYSLn\nE7H9276R1lvX8HZ0E2T4ea63PcVcTldw74ueEQr7HFMVucO+hHjgNJXDsWFUNppv\nxqWOvlIEDRdQzB1UYd13orqX0t514Ikp5Y3oNigXftDH+lZPlrWGsknMIDWr4DKP\n7NB1BZMfLFu/HXTGI9dK5Zc4T4GG4DBZqlgDPdzAXSBUT9cRQvbLrZ5+tUjOZK5E\nzEEIqyUo1+QdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABgtnWaWIDFCbKvhD8cF\nd5fvARFJcRl4dcChoqANeUXK4iNiCEPiDJb4xDGrLSVOeQ2IMbghqCwJfH93aqTr\n9kFQPvYbCt10TPQpmmh2//QjWGc7lxniFWR8pAVYdCGHqIAMvW2V2177quHsqc2I\nNTXyEUus0SDHLK8swLQxoCVw4fSq+kjMFW/3zOvMfh13rZH7Lo0gQyAUcuHM5U7g\nbhCZ3yVkDYpPxVv2vL0eyWUdLrQjYXyY7MWHPXvDozi3CtuBZlp6ulgeubi6jhHE\nIzuOM3qiLMJ/KG8MlIgGCwSX/x0vfO0/LtkZM7P1+yptSr/Se5QiZMtmpxC+DDWJ\n2xw=\n-----END CERTIFICATE-----"
   171  	t.Run("good url and CABundle value", func(t *testing.T) {
   172  		fas := webhookFixture()
   173  		url := "https://good.example.com"
   174  		fas.Spec.Policy.Webhook.URL = &url
   175  		fas.Spec.Policy.Webhook.Service = nil
   176  		fas.Spec.Policy.Webhook.CABundle = []byte(goodCaBundle)
   177  
   178  		causes := fas.Validate()
   179  		assert.Len(t, causes, 0)
   180  	})
   181  
   182  	t.Run("https url and invalid CABundle value", func(t *testing.T) {
   183  		fas := webhookFixture()
   184  		url := "https://bad.example.com"
   185  		fas.Spec.Policy.Webhook.URL = &url
   186  		fas.Spec.Policy.Webhook.Service = nil
   187  		fas.Spec.Policy.Webhook.CABundle = []byte("SomeInvalidCABundle")
   188  
   189  		causes := fas.Validate()
   190  		assert.Len(t, causes, 1)
   191  		assert.Equal(t, "spec.policy.webhook.caBundle", causes[0].Field)
   192  	})
   193  
   194  	t.Run("https url and missing CABundle value", func(t *testing.T) {
   195  		fas := webhookFixture()
   196  		url := "https://bad.example.com"
   197  		fas.Spec.Policy.Webhook.URL = &url
   198  		fas.Spec.Policy.Webhook.Service = nil
   199  		fas.Spec.Policy.Webhook.CABundle = nil
   200  
   201  		causes := fas.Validate()
   202  		assert.Len(t, causes, 1)
   203  		assert.Equal(t, "spec.policy.webhook.caBundle", causes[0].Field)
   204  	})
   205  
   206  	t.Run("bad url value", func(t *testing.T) {
   207  		fas := webhookFixture()
   208  		url := "http:/bad.example.com%"
   209  		fas.Spec.Policy.Webhook.URL = &url
   210  		fas.Spec.Policy.Webhook.Service = nil
   211  		fas.Spec.Policy.Webhook.CABundle = []byte(goodCaBundle)
   212  
   213  		causes := fas.Validate()
   214  		assert.Len(t, causes, 1)
   215  		assert.Equal(t, "spec.policy.webhook.url", causes[0].Field)
   216  	})
   217  
   218  }
   219  
   220  // nolint:dupl  // Linter errors on lines are duplicate of TestFleetAutoscalerListValidateUpdate
   221  func TestFleetAutoscalerCounterValidateUpdate(t *testing.T) {
   222  	t.Parallel()
   223  
   224  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   225  		fas := counterFixture()
   226  		f(&fas.Spec.Policy)
   227  		return fas
   228  	}
   229  
   230  	testCases := map[string]struct {
   231  		fas          *FleetAutoscaler
   232  		featureFlags string
   233  		wantLength   int
   234  		wantField    string
   235  	}{
   236  		"feature gate not turned on": {
   237  			fas:          counterFixture(),
   238  			featureFlags: string(runtime.FeatureCountsAndLists) + "=false",
   239  			wantLength:   1,
   240  			wantField:    "spec.policy.counter",
   241  		},
   242  		"nil parameters": {
   243  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   244  				fap.Counter = nil
   245  			}),
   246  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   247  			wantLength:   1,
   248  			wantField:    "spec.policy.counter",
   249  		},
   250  		"minCapacity size too large": {
   251  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   252  				fap.Counter.MinCapacity = int64(11)
   253  			}),
   254  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   255  			wantLength:   1,
   256  			wantField:    "spec.policy.counter.minCapacity",
   257  		},
   258  		"bufferSize size too small": {
   259  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   260  				fap.Counter.BufferSize = intstr.FromInt(0)
   261  			}),
   262  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   263  			wantLength:   1,
   264  			wantField:    "spec.policy.counter.bufferSize",
   265  		},
   266  		"maxCapacity size too small": {
   267  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   268  				fap.Counter.MaxCapacity = int64(4)
   269  			}),
   270  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   271  			wantLength:   1,
   272  			wantField:    "spec.policy.counter.maxCapacity",
   273  		},
   274  		"minCapacity size too small": {
   275  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   276  				fap.Counter.MinCapacity = int64(4)
   277  			}),
   278  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   279  			wantLength:   1,
   280  			wantField:    "spec.policy.counter.minCapacity",
   281  		},
   282  		"bufferSize percentage OK": {
   283  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   284  				fap.Counter.BufferSize.Type = intstr.String
   285  				fap.Counter.BufferSize = intstr.FromString("99%")
   286  				fap.Counter.MinCapacity = 10
   287  			}),
   288  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   289  			wantLength:   0,
   290  		},
   291  		"bufferSize percentage can't parse": {
   292  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   293  				fap.Counter.BufferSize.Type = intstr.String
   294  				fap.Counter.BufferSize = intstr.FromString("99.0%")
   295  				fap.Counter.MinCapacity = 1
   296  			}),
   297  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   298  			wantLength:   1,
   299  			wantField:    "spec.policy.counter.bufferSize",
   300  		},
   301  		"bufferSize percentage and MinCapacity too small": {
   302  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   303  				fap.Counter.BufferSize.Type = intstr.String
   304  				fap.Counter.BufferSize = intstr.FromString("0%")
   305  			}),
   306  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   307  			wantLength:   2,
   308  			wantField:    "spec.policy.counter.bufferSize",
   309  		},
   310  		"bufferSize percentage too large": {
   311  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   312  				fap.Counter.BufferSize.Type = intstr.String
   313  				fap.Counter.BufferSize = intstr.FromString("100%")
   314  				fap.Counter.MinCapacity = 10
   315  			}),
   316  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   317  			wantLength:   1,
   318  			wantField:    "spec.policy.counter.bufferSize",
   319  		},
   320  	}
   321  
   322  	runtime.FeatureTestMutex.Lock()
   323  	defer runtime.FeatureTestMutex.Unlock()
   324  
   325  	for name, tc := range testCases {
   326  		t.Run(name, func(t *testing.T) {
   327  			err := runtime.ParseFeatures(tc.featureFlags)
   328  			assert.NoError(t, err)
   329  
   330  			causes := tc.fas.Validate()
   331  
   332  			assert.Len(t, causes, tc.wantLength)
   333  			if tc.wantLength > 0 {
   334  				assert.Equal(t, tc.wantField, causes[0].Field)
   335  			}
   336  		})
   337  	}
   338  }
   339  
   340  // nolint:dupl  // Linter errors on lines are duplicate of TestFleetAutoscalerCounterValidateUpdate
   341  func TestFleetAutoscalerListValidateUpdate(t *testing.T) {
   342  	t.Parallel()
   343  
   344  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   345  		fas := listFixture()
   346  		f(&fas.Spec.Policy)
   347  		return fas
   348  	}
   349  
   350  	testCases := map[string]struct {
   351  		fas          *FleetAutoscaler
   352  		featureFlags string
   353  		wantLength   int
   354  		wantField    string
   355  	}{
   356  		"feature gate not turned on": {
   357  			fas:          listFixture(),
   358  			featureFlags: string(runtime.FeatureCountsAndLists) + "=false",
   359  			wantLength:   1,
   360  			wantField:    "spec.policy.list",
   361  		},
   362  		"nil parameters": {
   363  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   364  				fap.List = nil
   365  			}),
   366  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   367  			wantLength:   1,
   368  			wantField:    "spec.policy.list",
   369  		},
   370  		"minCapacity size too large": {
   371  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   372  				fap.List.MinCapacity = int64(11)
   373  			}),
   374  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   375  			wantLength:   1,
   376  			wantField:    "spec.policy.list.minCapacity",
   377  		},
   378  		"bufferSize size too small": {
   379  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   380  				fap.List.BufferSize = intstr.FromInt(0)
   381  			}),
   382  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   383  			wantLength:   1,
   384  			wantField:    "spec.policy.list.bufferSize",
   385  		},
   386  		"maxCapacity size too small": {
   387  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   388  				fap.List.MaxCapacity = int64(4)
   389  			}),
   390  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   391  			wantLength:   1,
   392  			wantField:    "spec.policy.list.maxCapacity",
   393  		},
   394  		"minCapacity size too small": {
   395  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   396  				fap.List.MinCapacity = int64(4)
   397  			}),
   398  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   399  			wantLength:   1,
   400  			wantField:    "spec.policy.list.minCapacity",
   401  		},
   402  		"bufferSize percentage OK": {
   403  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   404  				fap.List.BufferSize.Type = intstr.String
   405  				fap.List.BufferSize = intstr.FromString("99%")
   406  				fap.List.MinCapacity = 1
   407  			}),
   408  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   409  			wantLength:   0,
   410  		},
   411  		"bufferSize percentage can't parse": {
   412  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   413  				fap.List.BufferSize.Type = intstr.String
   414  				fap.List.BufferSize = intstr.FromString("99.0%")
   415  				fap.List.MinCapacity = 1
   416  			}),
   417  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   418  			wantLength:   1,
   419  			wantField:    "spec.policy.list.bufferSize",
   420  		},
   421  		"bufferSize percentage and MinCapacity too small": {
   422  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   423  				fap.List.BufferSize.Type = intstr.String
   424  				fap.List.BufferSize = intstr.FromString("0%")
   425  			}),
   426  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   427  			wantLength:   2,
   428  			wantField:    "spec.policy.list.bufferSize",
   429  		},
   430  		"bufferSize percentage too large": {
   431  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   432  				fap.List.BufferSize.Type = intstr.String
   433  				fap.List.BufferSize = intstr.FromString("100%")
   434  				fap.List.MinCapacity = 1
   435  			}),
   436  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   437  			wantLength:   1,
   438  			wantField:    "spec.policy.list.bufferSize",
   439  		},
   440  	}
   441  
   442  	runtime.FeatureTestMutex.Lock()
   443  	defer runtime.FeatureTestMutex.Unlock()
   444  
   445  	for name, tc := range testCases {
   446  		t.Run(name, func(t *testing.T) {
   447  			err := runtime.ParseFeatures(tc.featureFlags)
   448  			assert.NoError(t, err)
   449  
   450  			causes := tc.fas.Validate()
   451  
   452  			assert.Len(t, causes, tc.wantLength)
   453  			if tc.wantLength > 0 {
   454  				assert.Equal(t, tc.wantField, causes[0].Field)
   455  			}
   456  		})
   457  	}
   458  }
   459  
   460  func TestFleetAutoscalerScheduleValidateUpdate(t *testing.T) {
   461  	t.Parallel()
   462  
   463  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   464  		fas := scheduleFixture()
   465  		f(&fas.Spec.Policy)
   466  		return fas
   467  	}
   468  
   469  	testCases := map[string]struct {
   470  		fas          *FleetAutoscaler
   471  		featureFlags string
   472  		wantLength   int
   473  		wantField    string
   474  	}{
   475  		"feature gate not turned on": {
   476  			fas:          scheduleFixture(),
   477  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=false",
   478  			wantLength:   1,
   479  			wantField:    "spec.policy.schedule",
   480  		},
   481  		"end time before current time": {
   482  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   483  				fap.Schedule.Between.End = mustParseDate("2024-07-03T15:59:59Z")
   484  			}),
   485  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   486  			wantLength:   1,
   487  			wantField:    "spec.policy.schedule.between.end",
   488  		},
   489  		"end time before start time": {
   490  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   491  				fap.Schedule.Between.Start = mustParseDate("3999-06-15T15:59:59Z")
   492  				fap.Schedule.Between.End = mustParseDate("3999-05-15T15:59:59Z")
   493  			}),
   494  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   495  			wantLength:   1,
   496  			wantField:    "spec.policy.schedule.between",
   497  		},
   498  		"invalid cron format": {
   499  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   500  				fap.Schedule.ActivePeriod.StartCron = "* * * * * * * *"
   501  			}),
   502  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   503  			wantLength:   1,
   504  			wantField:    "spec.policy.schedule.activePeriod.startCron",
   505  		},
   506  		"invalid cron format with tz specification": {
   507  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   508  				fap.Schedule.ActivePeriod.StartCron = "CRON_TZ=America/New_York * * * * *"
   509  			}),
   510  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   511  			wantLength:   1,
   512  			wantField:    "spec.policy.schedule.activePeriod.startCron",
   513  		},
   514  	}
   515  
   516  	runtime.FeatureTestMutex.Lock()
   517  	defer runtime.FeatureTestMutex.Unlock()
   518  
   519  	for name, tc := range testCases {
   520  		t.Run(name, func(t *testing.T) {
   521  			err := runtime.ParseFeatures(tc.featureFlags)
   522  			assert.NoError(t, err)
   523  
   524  			causes := tc.fas.Validate()
   525  
   526  			assert.Len(t, causes, tc.wantLength)
   527  			if tc.wantLength > 0 {
   528  				assert.Equal(t, tc.wantField, causes[0].Field)
   529  			}
   530  		})
   531  	}
   532  }
   533  
   534  func TestFleetAutoscalerChainValidateUpdate(t *testing.T) {
   535  	t.Parallel()
   536  
   537  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   538  		fas := chainFixture()
   539  		f(&fas.Spec.Policy)
   540  		return fas
   541  	}
   542  
   543  	testCases := map[string]struct {
   544  		fas          *FleetAutoscaler
   545  		featureFlags string
   546  		wantLength   int
   547  		wantField    string
   548  	}{
   549  		"feature gate not turned on": {
   550  			fas:          chainFixture(),
   551  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=false",
   552  			wantLength:   1,
   553  			wantField:    "spec.policy.chain",
   554  		},
   555  		"duplicate id": {
   556  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   557  				fap.Chain[1].ID = "weekends"
   558  			}),
   559  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   560  			wantLength:   1,
   561  			wantField:    "spec.policy.chain",
   562  		},
   563  		"missing policy": {
   564  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   565  				fap.Chain[0].FleetAutoscalerPolicy = FleetAutoscalerPolicy{}
   566  			}),
   567  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   568  			wantLength:   1,
   569  			wantField:    "spec.policy.chain[0]",
   570  		},
   571  		"nested chain policy not allowed": {
   572  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   573  				fap.Chain[1].FleetAutoscalerPolicy = FleetAutoscalerPolicy{
   574  					Type: ChainPolicyType,
   575  					Chain: ChainPolicy{
   576  						{
   577  							FleetAutoscalerPolicy: FleetAutoscalerPolicy{
   578  								Type: BufferPolicyType,
   579  								Buffer: &BufferPolicy{
   580  									BufferSize:  intstr.FromInt(5),
   581  									MaxReplicas: 10,
   582  								},
   583  							},
   584  						},
   585  					},
   586  				}
   587  			}),
   588  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   589  			wantLength:   1,
   590  			wantField:    "spec.policy.chain[1]",
   591  		},
   592  		"invalid nested policy format": {
   593  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   594  				fap.Chain[1].FleetAutoscalerPolicy.Buffer.MinReplicas = 20
   595  			}),
   596  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   597  			wantLength:   1,
   598  			wantField:    "spec.policy.chain[1].policy.buffer.minReplicas",
   599  		},
   600  	}
   601  
   602  	runtime.FeatureTestMutex.Lock()
   603  	defer runtime.FeatureTestMutex.Unlock()
   604  
   605  	for name, tc := range testCases {
   606  		t.Run(name, func(t *testing.T) {
   607  			err := runtime.ParseFeatures(tc.featureFlags)
   608  			assert.NoError(t, err)
   609  
   610  			causes := tc.fas.Validate()
   611  
   612  			assert.Len(t, causes, tc.wantLength)
   613  			if tc.wantLength > 0 {
   614  				assert.Equal(t, tc.wantField, causes[0].Field)
   615  			}
   616  		})
   617  	}
   618  }
   619  
   620  func TestFleetAutoscalerWasmValidateUpdate(t *testing.T) {
   621  	t.Parallel()
   622  
   623  	testCases := map[string]struct {
   624  		fas          *FleetAutoscaler
   625  		featureFlags string
   626  		wantLength   int
   627  		wantField    string
   628  	}{
   629  		"valid": {
   630  			fas:          wasmFixture(),
   631  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   632  			wantLength:   0,
   633  		},
   634  		"feature gate not turned on": {
   635  			fas:          wasmFixture(),
   636  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=false",
   637  			wantLength:   1,
   638  			wantField:    "spec.policy.wasm",
   639  		},
   640  		"nil wasm policy": {
   641  			fas: func() *FleetAutoscaler {
   642  				fas := wasmFixture()
   643  				fas.Spec.Policy.Wasm = nil
   644  				return fas
   645  			}(),
   646  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   647  			wantLength:   1,
   648  			wantField:    "spec.policy.wasm",
   649  		},
   650  		"nil from.url": {
   651  			fas: func() *FleetAutoscaler {
   652  				fas := wasmFixture()
   653  				fas.Spec.Policy.Wasm.From.URL = nil
   654  				return fas
   655  			}(),
   656  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   657  			wantLength:   1,
   658  			wantField:    "spec.policy.wasm.from",
   659  		},
   660  		"invalid url": {
   661  			fas: func() *FleetAutoscaler {
   662  				fas := wasmFixture()
   663  				badURL := "://invalid-url"
   664  				fas.Spec.Policy.Wasm.From.URL.URL = &badURL
   665  				return fas
   666  			}(),
   667  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   668  			wantLength:   1,
   669  			wantField:    "spec.policy.wasm.from.url.url",
   670  		},
   671  	}
   672  
   673  	runtime.FeatureTestMutex.Lock()
   674  	defer runtime.FeatureTestMutex.Unlock()
   675  
   676  	for name, tc := range testCases {
   677  		t.Run(name, func(t *testing.T) {
   678  			err := runtime.ParseFeatures(tc.featureFlags)
   679  			assert.NoError(t, err)
   680  
   681  			causes := tc.fas.Validate()
   682  
   683  			assert.Len(t, causes, tc.wantLength)
   684  			if tc.wantLength > 0 && len(causes) > 0 {
   685  				assert.Equal(t, tc.wantField, causes[0].Field)
   686  			}
   687  		})
   688  	}
   689  }
   690  
   691  func TestFleetAutoscalerApplyDefaults(t *testing.T) {
   692  	fas := &FleetAutoscaler{}
   693  
   694  	// gate
   695  	assert.Nil(t, fas.Spec.Sync)
   696  
   697  	fas.ApplyDefaults()
   698  	assert.NotNil(t, fas.Spec.Sync)
   699  	assert.Equal(t, FixedIntervalSyncType, fas.Spec.Sync.Type)
   700  	assert.Equal(t, defaultIntervalSyncSeconds, fas.Spec.Sync.FixedInterval.Seconds)
   701  
   702  	// Test apply defaults is idempotent -- calling ApplyDefaults more than one time does not change the original result.
   703  	fas.ApplyDefaults()
   704  	assert.NotNil(t, fas.Spec.Sync)
   705  	assert.Equal(t, FixedIntervalSyncType, fas.Spec.Sync.Type)
   706  	assert.Equal(t, defaultIntervalSyncSeconds, fas.Spec.Sync.FixedInterval.Seconds)
   707  }
   708  
   709  func mustParseDate(timeStr string) metav1.Time {
   710  	t, _ := time.Parse(time.RFC3339, timeStr)
   711  	return metav1.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
   712  }
   713  
   714  func defaultFixture() *FleetAutoscaler {
   715  	return customFixture(BufferPolicyType)
   716  }
   717  
   718  func webhookFixture() *FleetAutoscaler {
   719  	return customFixture(WebhookPolicyType)
   720  }
   721  
   722  func counterFixture() *FleetAutoscaler {
   723  	return customFixture(CounterPolicyType)
   724  }
   725  
   726  func listFixture() *FleetAutoscaler {
   727  	return customFixture(ListPolicyType)
   728  }
   729  
   730  func scheduleFixture() *FleetAutoscaler {
   731  	return customFixture(SchedulePolicyType)
   732  }
   733  
   734  func chainFixture() *FleetAutoscaler {
   735  	return customFixture(ChainPolicyType)
   736  }
   737  
   738  func wasmFixture() *FleetAutoscaler {
   739  	return customFixture(WasmPolicyType)
   740  }
   741  
   742  func customFixture(t FleetAutoscalerPolicyType) *FleetAutoscaler {
   743  
   744  	res := &FleetAutoscaler{
   745  		ObjectMeta: metav1.ObjectMeta{Name: "test"},
   746  		Spec: FleetAutoscalerSpec{
   747  			FleetName: "testing",
   748  			Policy: FleetAutoscalerPolicy{
   749  				Type: BufferPolicyType,
   750  				Buffer: &BufferPolicy{
   751  					BufferSize:  intstr.FromInt(5),
   752  					MaxReplicas: 10,
   753  				},
   754  			},
   755  			Sync: &FleetAutoscalerSync{
   756  				Type: FixedIntervalSyncType,
   757  				FixedInterval: FixedIntervalSync{
   758  					Seconds: 30,
   759  				},
   760  			},
   761  		},
   762  	}
   763  	switch t {
   764  	case BufferPolicyType:
   765  	case WebhookPolicyType:
   766  		res.Spec.Policy.Type = WebhookPolicyType
   767  		res.Spec.Policy.Buffer = nil
   768  		url := "/scale"
   769  		res.Spec.Policy.Webhook = &URLConfiguration{
   770  			Service: &admregv1.ServiceReference{
   771  				Name:      "service1",
   772  				Namespace: "default",
   773  				Path:      &url,
   774  			},
   775  		}
   776  	case CounterPolicyType:
   777  		res.Spec.Policy.Type = CounterPolicyType
   778  		res.Spec.Policy.Buffer = nil
   779  		res.Spec.Policy.Counter = &CounterPolicy{
   780  			BufferSize:  intstr.FromInt(5),
   781  			MaxCapacity: 10,
   782  		}
   783  	case ListPolicyType:
   784  		res.Spec.Policy.Type = ListPolicyType
   785  		res.Spec.Policy.Buffer = nil
   786  		res.Spec.Policy.List = &ListPolicy{
   787  			BufferSize:  intstr.FromInt(5),
   788  			MaxCapacity: 10,
   789  		}
   790  	case SchedulePolicyType:
   791  		res.Spec.Policy.Type = SchedulePolicyType
   792  		res.Spec.Policy.Buffer = nil
   793  		res.Spec.Policy.Schedule = &SchedulePolicy{
   794  			Between: Between{
   795  				Start: mustParseDate("2024-07-01T15:59:59Z"),
   796  				End:   mustParseDate("9999-07-03T15:59:59Z"),
   797  			},
   798  			ActivePeriod: ActivePeriod{
   799  				Timezone:  "",
   800  				StartCron: "* * * * 1-5",
   801  				Duration:  "",
   802  			},
   803  			Policy: FleetAutoscalerPolicy{
   804  				Type: BufferPolicyType,
   805  				Buffer: &BufferPolicy{
   806  					BufferSize:  intstr.FromInt(5),
   807  					MaxReplicas: 10,
   808  				},
   809  			},
   810  		}
   811  	case ChainPolicyType:
   812  		res.Spec.Policy.Type = ChainPolicyType
   813  		res.Spec.Policy.Buffer = nil
   814  		res.Spec.Policy.Chain = ChainPolicy{
   815  			{
   816  				ID: "weekends",
   817  				FleetAutoscalerPolicy: FleetAutoscalerPolicy{
   818  					Type: SchedulePolicyType,
   819  					Schedule: &SchedulePolicy{
   820  						Between: Between{
   821  							Start: mustParseDate("2024-07-04T15:59:59Z"),
   822  							End:   mustParseDate("9999-07-05T15:59:59Z"),
   823  						},
   824  						ActivePeriod: ActivePeriod{
   825  							Timezone:  "",
   826  							StartCron: "* * * * 5-6",
   827  							Duration:  "",
   828  						},
   829  						Policy: FleetAutoscalerPolicy{
   830  							Type: CounterPolicyType,
   831  							Counter: &CounterPolicy{
   832  								Key:         "playerCount",
   833  								BufferSize:  intstr.FromInt32(5),
   834  								MaxCapacity: 10,
   835  							},
   836  						},
   837  					},
   838  				},
   839  			},
   840  			{
   841  				ID: "",
   842  				FleetAutoscalerPolicy: FleetAutoscalerPolicy{
   843  					Type: BufferPolicyType,
   844  					Buffer: &BufferPolicy{
   845  						BufferSize:  intstr.FromInt32(5),
   846  						MaxReplicas: 10,
   847  					},
   848  				},
   849  			},
   850  		}
   851  	case WasmPolicyType:
   852  		res.Spec.Policy.Type = WasmPolicyType
   853  		res.Spec.Policy.Buffer = nil
   854  		url := "http://example.com/wasm-module"
   855  		res.Spec.Policy.Wasm = &WasmPolicy{
   856  			Function: "scale",
   857  			Config: map[string]string{
   858  				"scale_buffer": "10",
   859  			},
   860  			From: WasmFrom{
   861  				URL: &URLConfiguration{
   862  					URL: &url,
   863  				},
   864  			},
   865  		}
   866  	}
   867  	return res
   868  }