diff --git a/pkg/util/context_test.go b/pkg/util/context_test.go new file mode 100644 index 000000000000..5c601495fd44 --- /dev/null +++ b/pkg/util/context_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestContextForChannel(t *testing.T) { + tests := []struct { + name string + setup func() (chan struct{}, func(context.Context, context.CancelFunc)) + expectedDone bool + timeout time.Duration + }{ + { + name: "context is cancelled when cancel function is called", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + return ch, func(_ context.Context, cancel context.CancelFunc) { + cancel() + } + }, + expectedDone: true, + timeout: time.Second, + }, + { + name: "context is cancelled when parent channel is closed", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + return ch, func(_ context.Context, _ context.CancelFunc) { + close(ch) + } + }, + expectedDone: true, + timeout: time.Second, + }, + { + name: "context remains open when neither cancelled nor parent channel closed", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + return ch, func(_ context.Context, _ context.CancelFunc) { + // Do nothing - context should remain open + } + }, + expectedDone: false, + timeout: 100 * time.Millisecond, + }, + { + name: "concurrent operations - cancel first, then close parent", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + return ch, func(_ context.Context, cancel context.CancelFunc) { + go cancel() + go func() { + time.Sleep(50 * time.Millisecond) + close(ch) + }() + } + }, + expectedDone: true, + timeout: time.Second, + }, + { + name: "concurrent operations - close parent first, then cancel", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + return ch, func(_ context.Context, cancel context.CancelFunc) { + go close(ch) + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + } + }, + expectedDone: true, + timeout: time.Second, + }, + { + name: "multiple cancel calls should not panic", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + return ch, func(_ context.Context, cancel context.CancelFunc) { + cancel() + cancel() // Second call should not panic + cancel() // Third call should not panic + } + }, + expectedDone: true, + timeout: time.Second, + }, + { + name: "parent channel already closed", + setup: func() (chan struct{}, func(context.Context, context.CancelFunc)) { + ch := make(chan struct{}) + close(ch) + return ch, func(_ context.Context, _ context.CancelFunc) {} + }, + expectedDone: true, + timeout: time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parentCh, operation := tt.setup() + ctx, cancel := ContextForChannel(parentCh) + defer cancel() // Always clean up + + // Run the test operation + operation(ctx, cancel) + + // Check if context is done within timeout + select { + case <-ctx.Done(): + assert.True(t, tt.expectedDone, "context was cancelled but expected to remain open") + case <-time.After(tt.timeout): + assert.False(t, tt.expectedDone, "context remained open but expected to be cancelled") + } + }) + } +} diff --git a/pkg/util/policy_test.go b/pkg/util/policy_test.go new file mode 100644 index 000000000000..a924e4e2dedc --- /dev/null +++ b/pkg/util/policy_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" +) + +func TestIsLazyActivationEnabled(t *testing.T) { + tests := []struct { + name string + activationPreference policyv1alpha1.ActivationPreference + expected bool + }{ + { + name: "empty activation preference", + activationPreference: "", + expected: false, + }, + { + name: "lazy activation enabled", + activationPreference: policyv1alpha1.LazyActivation, + expected: true, + }, + { + name: "different activation preference", + activationPreference: "SomeOtherPreference", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsLazyActivationEnabled(tt.activationPreference) + assert.Equal(t, tt.expected, result, "unexpected result for activation preference: %s", tt.activationPreference) + }) + } +} diff --git a/pkg/util/round_trippers_test.go b/pkg/util/round_trippers_test.go new file mode 100644 index 000000000000..e28ff85c1c3b --- /dev/null +++ b/pkg/util/round_trippers_test.go @@ -0,0 +1,242 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/client-go/transport" +) + +func TestNewProxyHeaderRoundTripperWrapperConstructor(t *testing.T) { + tests := []struct { + name string + wrapperFunc transport.WrapperFunc + headers map[string]string + expectedEmpty bool + expectedCount int + expectedHeader string + expectedValues []string + }{ + { + name: "nil wrapper with empty headers", + wrapperFunc: nil, + headers: nil, + expectedEmpty: true, + }, + { + name: "nil wrapper with single header", + wrapperFunc: nil, + headers: map[string]string{ + "Proxy-Authorization": "Basic xyz", + }, + expectedCount: 1, + expectedHeader: "Proxy-Authorization", + expectedValues: []string{"Basic xyz"}, + }, + { + name: "nil wrapper with multiple comma-separated values", + wrapperFunc: nil, + headers: map[string]string{ + "X-Custom-Header": "value1,value2,value3", + }, + expectedCount: 1, + expectedHeader: "X-Custom-Header", + expectedValues: []string{"value1", "value2", "value3"}, + }, + { + name: "with wrapper func", + wrapperFunc: func(rt http.RoundTripper) http.RoundTripper { + return rt + }, + headers: map[string]string{ + "Proxy-Authorization": "Basic abc", + }, + expectedCount: 1, + expectedHeader: "Proxy-Authorization", + expectedValues: []string{"Basic abc"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wrapper := NewProxyHeaderRoundTripperWrapperConstructor(tt.wrapperFunc, tt.headers) + assert.NotNil(t, wrapper, "wrapper should not be nil") + + mockRT := &mockRoundTripper{} + rt := wrapper(mockRT) + phrt, ok := rt.(*proxyHeaderRoundTripper) + assert.True(t, ok, "should be able to cast to proxyHeaderRoundTripper") + + if tt.expectedEmpty { + assert.Empty(t, phrt.proxyHeaders, "proxy headers should be empty") + return + } + + assert.Equal(t, tt.expectedCount, len(phrt.proxyHeaders), "should have expected number of headers") + assert.Equal(t, tt.expectedValues, phrt.proxyHeaders[tt.expectedHeader], "should have expected header values") + }) + } +} + +func TestRoundTrip(t *testing.T) { + tests := []struct { + name string + roundTripper http.RoundTripper + headers map[string]string + expectedError bool + expectedStatus int + }{ + { + name: "with http transport", + roundTripper: &http.Transport{ + ProxyConnectHeader: make(http.Header), + }, + headers: map[string]string{ + "Proxy-Authorization": "Basic xyz", + }, + expectedStatus: http.StatusOK, + }, + { + name: "with custom round tripper", + roundTripper: &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusOK, + }, + }, + headers: map[string]string{ + "Custom-Header": "value", + }, + expectedStatus: http.StatusOK, + }, + { + name: "with error", + roundTripper: &mockRoundTripper{ + err: assert.AnError, + }, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + phrt := &proxyHeaderRoundTripper{ + proxyHeaders: parseProxyHeaders(tt.headers), + roundTripper: tt.roundTripper, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + assert.NoError(t, err, "should create request without error") + + resp, err := phrt.RoundTrip(req) + + if tt.expectedError { + assert.Error(t, err, "should return error") + assert.Nil(t, resp, "response should be nil") + return + } + + assert.NoError(t, err, "should not return error") + assert.NotNil(t, resp, "response should not be nil") + assert.Equal(t, tt.expectedStatus, resp.StatusCode, "should have expected status code") + }) + } +} + +func TestParseProxyHeaders(t *testing.T) { + tests := []struct { + name string + headers map[string]string + expectedEmpty bool + expectedCount int + expectedHeader string + expectedValues []string + }{ + { + name: "nil headers", + headers: nil, + expectedEmpty: true, + }, + { + name: "empty headers", + headers: map[string]string{}, + expectedEmpty: true, + }, + { + name: "single header", + headers: map[string]string{ + "proxy-authorization": "Basic xyz", + }, + expectedCount: 1, + expectedHeader: "Proxy-Authorization", + expectedValues: []string{"Basic xyz"}, + }, + { + name: "multiple comma-separated values", + headers: map[string]string{ + "x-custom-header": "value1,value2,value3", + }, + expectedCount: 1, + expectedHeader: "X-Custom-Header", + expectedValues: []string{"value1", "value2", "value3"}, + }, + { + name: "multiple headers", + headers: map[string]string{ + "proxy-authorization": "Basic xyz", + "x-custom-header": "value1,value2", + }, + expectedCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseProxyHeaders(tt.headers) + + if tt.expectedEmpty { + assert.Nil(t, result, "headers should be nil") + return + } + + assert.Equal(t, tt.expectedCount, len(result), "should have expected number of headers") + + if tt.expectedHeader != "" { + assert.Equal(t, tt.expectedValues, result[tt.expectedHeader], "should have expected header values") + } + }) + } +} + +// Mock Implementations + +type mockRoundTripper struct { + response *http.Response + err error +} + +func (m *mockRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { + return m.response, m.err +}