| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- /*
- Copyright © The ESO 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
- https://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 doppler
- import (
- "errors"
- "net/url"
- "testing"
- "time"
- "github.com/external-secrets/external-secrets/providers/v1/doppler/client"
- )
- const testSecretValue = "value"
- // mockClient implements SecretsClientInterface for testing retry logic.
- type mockClient struct {
- authenticateCalls int
- getSecretCalls int
- getSecretsCalls int
- updateSecretsCalls int
- failUntilCall int
- returnError error
- secretResponse *client.SecretResponse
- secretsResponse *client.SecretsResponse
- }
- func (m *mockClient) BaseURL() *url.URL {
- return &url.URL{Scheme: "https", Host: "api.doppler.com"}
- }
- func (m *mockClient) Authenticate() error {
- m.authenticateCalls++
- if m.authenticateCalls < m.failUntilCall {
- return m.returnError
- }
- return nil
- }
- func (m *mockClient) GetSecret(_ client.SecretRequest) (*client.SecretResponse, error) {
- m.getSecretCalls++
- if m.getSecretCalls < m.failUntilCall {
- return nil, m.returnError
- }
- return m.secretResponse, nil
- }
- func (m *mockClient) GetSecrets(_ client.SecretsRequest) (*client.SecretsResponse, error) {
- m.getSecretsCalls++
- if m.getSecretsCalls < m.failUntilCall {
- return nil, m.returnError
- }
- return m.secretsResponse, nil
- }
- func (m *mockClient) UpdateSecrets(_ client.UpdateSecretsRequest) error {
- m.updateSecretsCalls++
- if m.updateSecretsCalls < m.failUntilCall {
- return m.returnError
- }
- return nil
- }
- func TestRetryClientSuccessOnFirstAttempt(t *testing.T) {
- mock := &mockClient{
- failUntilCall: 1, // succeed on first call
- secretResponse: &client.SecretResponse{Name: "test", Value: testSecretValue},
- secretsResponse: &client.SecretsResponse{Secrets: client.Secrets{"test": testSecretValue}},
- }
- retryClient := newRetryableClient(mock, 3, 10*time.Millisecond)
- // Test Authenticate
- if err := retryClient.Authenticate(); err != nil {
- t.Errorf("Authenticate should succeed on first attempt, got error: %v", err)
- }
- if mock.authenticateCalls != 1 {
- t.Errorf("Expected 1 authenticate call, got %d", mock.authenticateCalls)
- }
- // Test GetSecret
- resp, err := retryClient.GetSecret(client.SecretRequest{Name: "test"})
- if err != nil {
- t.Errorf("GetSecret should succeed on first attempt, got error: %v", err)
- }
- if resp == nil || resp.Value != testSecretValue {
- t.Errorf("GetSecret returned unexpected response: %v", resp)
- }
- if mock.getSecretCalls != 1 {
- t.Errorf("Expected 1 getSecret call, got %d", mock.getSecretCalls)
- }
- // Test GetSecrets
- respSecrets, err := retryClient.GetSecrets(client.SecretsRequest{})
- if err != nil {
- t.Errorf("GetSecrets should succeed on first attempt, got error: %v", err)
- }
- if respSecrets == nil || respSecrets.Secrets["test"] != testSecretValue {
- t.Errorf("GetSecrets returned unexpected response: %v", respSecrets)
- }
- if mock.getSecretsCalls != 1 {
- t.Errorf("Expected 1 getSecrets call, got %d", mock.getSecretsCalls)
- }
- // Test UpdateSecrets
- if err := retryClient.UpdateSecrets(client.UpdateSecretsRequest{}); err != nil {
- t.Errorf("UpdateSecrets should succeed on first attempt, got error: %v", err)
- }
- if mock.updateSecretsCalls != 1 {
- t.Errorf("Expected 1 updateSecrets call, got %d", mock.updateSecretsCalls)
- }
- }
- func TestRetryClientSuccessAfterRetries(t *testing.T) {
- testError := errors.New("temporary error")
- mock := &mockClient{
- failUntilCall: 3, // fail twice, succeed on third attempt
- returnError: testError,
- secretResponse: &client.SecretResponse{Name: "test", Value: testSecretValue},
- secretsResponse: &client.SecretsResponse{Secrets: client.Secrets{"test": testSecretValue}},
- }
- retryClient := newRetryableClient(mock, 5, 1*time.Millisecond)
- // Test Authenticate - should retry and eventually succeed
- if err := retryClient.Authenticate(); err != nil {
- t.Errorf("Authenticate should succeed after retries, got error: %v", err)
- }
- if mock.authenticateCalls != 3 {
- t.Errorf("Expected 3 authenticate calls (2 failures + 1 success), got %d", mock.authenticateCalls)
- }
- // Reset for GetSecret test
- mock.getSecretCalls = 0
- resp, err := retryClient.GetSecret(client.SecretRequest{Name: "test"})
- if err != nil {
- t.Errorf("GetSecret should succeed after retries, got error: %v", err)
- }
- if resp == nil || resp.Value != testSecretValue {
- t.Errorf("GetSecret returned unexpected response: %v", resp)
- }
- if mock.getSecretCalls != 3 {
- t.Errorf("Expected 3 getSecret calls (2 failures + 1 success), got %d", mock.getSecretCalls)
- }
- }
- func TestRetryClientFailureAfterMaxRetries(t *testing.T) {
- testError := errors.New("persistent error")
- mock := &mockClient{
- failUntilCall: 100, // always fail
- returnError: testError,
- }
- retryClient := newRetryableClient(mock, 3, 1*time.Millisecond)
- // Test Authenticate - should fail after max retries
- err := retryClient.Authenticate()
- if err == nil {
- t.Error("Authenticate should fail after max retries")
- }
- if !errors.Is(err, testError) {
- t.Errorf("Expected error %v, got %v", testError, err)
- }
- // With maxRetries=3, backoff.Steps=3 means 3 total attempts
- if mock.authenticateCalls != 3 {
- t.Errorf("Expected 3 authenticate calls, got %d", mock.authenticateCalls)
- }
- // Reset for GetSecret test
- mock.getSecretCalls = 0
- _, err = retryClient.GetSecret(client.SecretRequest{Name: "test"})
- if err == nil {
- t.Error("GetSecret should fail after max retries")
- }
- if !errors.Is(err, testError) {
- t.Errorf("Expected error %v, got %v", testError, err)
- }
- if mock.getSecretCalls != 3 {
- t.Errorf("Expected 3 getSecret calls, got %d", mock.getSecretCalls)
- }
- }
- func TestRetryClientRetryInterval(t *testing.T) {
- testError := errors.New("temporary error")
- mock := &mockClient{
- failUntilCall: 3, // fail twice
- returnError: testError,
- }
- retryInterval := 20 * time.Millisecond
- retryClient := newRetryableClient(mock, 5, retryInterval)
- start := time.Now()
- _ = retryClient.Authenticate()
- elapsed := time.Since(start)
- // Should have waited at least retryInterval (for the first retry)
- // Note: DefaultBackoff has Factor=5.0 and Jitter=0.1, so delays increase exponentially
- // We just verify it took some reasonable time with retries
- minExpected := retryInterval
- if elapsed < minExpected {
- t.Errorf("Expected at least %v elapsed time for retries, got %v", minExpected, elapsed)
- }
- // Sanity check - with exponential backoff (Factor=5.0), shouldn't take too excessively long
- // First retry: ~20ms, Second retry: ~100ms (20ms * 5), Total: ~120ms + execution time
- maxExpected := 1 * time.Second
- if elapsed > maxExpected {
- t.Errorf("Expected less than %v elapsed time, got %v (may indicate exponential backoff issue)", maxExpected, elapsed)
- }
- }
- func TestRetryClientBaseURL(t *testing.T) {
- mock := &mockClient{}
- retryClient := newRetryableClient(mock, 3, 10*time.Millisecond)
- baseURL := retryClient.BaseURL()
- if baseURL == nil {
- t.Error("BaseURL should not be nil")
- }
- if baseURL.Host != "api.doppler.com" {
- t.Errorf("Expected host 'api.doppler.com', got '%s'", baseURL.Host)
- }
- }
- func TestRetryClientZeroRetries(t *testing.T) {
- testError := errors.New("error")
- mock := &mockClient{
- failUntilCall: 5, // fail multiple times
- returnError: testError,
- }
- // maxRetries = 0 means we don't override Steps, so it uses DefaultBackoff.Steps = 4
- retryClient := newRetryableClient(mock, 0, 1*time.Millisecond)
- err := retryClient.Authenticate()
- if err == nil {
- t.Error("Expected error with failing calls")
- }
- // With maxRetries=0, Steps is not overridden, so it uses DefaultBackoff.Steps=4
- if mock.authenticateCalls != 4 {
- t.Errorf("Expected 4 authenticate calls (DefaultBackoff.Steps), got %d", mock.authenticateCalls)
- }
- }
|