retry_client_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /*
  2. Copyright © The ESO Authors
  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. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package doppler
  14. import (
  15. "errors"
  16. "net/url"
  17. "testing"
  18. "time"
  19. "github.com/external-secrets/external-secrets/providers/v1/doppler/client"
  20. )
  21. const testSecretValue = "value"
  22. // mockClient implements SecretsClientInterface for testing retry logic.
  23. type mockClient struct {
  24. authenticateCalls int
  25. getSecretCalls int
  26. getSecretsCalls int
  27. updateSecretsCalls int
  28. failUntilCall int
  29. returnError error
  30. secretResponse *client.SecretResponse
  31. secretsResponse *client.SecretsResponse
  32. }
  33. func (m *mockClient) BaseURL() *url.URL {
  34. return &url.URL{Scheme: "https", Host: "api.doppler.com"}
  35. }
  36. func (m *mockClient) Authenticate() error {
  37. m.authenticateCalls++
  38. if m.authenticateCalls < m.failUntilCall {
  39. return m.returnError
  40. }
  41. return nil
  42. }
  43. func (m *mockClient) GetSecret(_ client.SecretRequest) (*client.SecretResponse, error) {
  44. m.getSecretCalls++
  45. if m.getSecretCalls < m.failUntilCall {
  46. return nil, m.returnError
  47. }
  48. return m.secretResponse, nil
  49. }
  50. func (m *mockClient) GetSecrets(_ client.SecretsRequest) (*client.SecretsResponse, error) {
  51. m.getSecretsCalls++
  52. if m.getSecretsCalls < m.failUntilCall {
  53. return nil, m.returnError
  54. }
  55. return m.secretsResponse, nil
  56. }
  57. func (m *mockClient) UpdateSecrets(_ client.UpdateSecretsRequest) error {
  58. m.updateSecretsCalls++
  59. if m.updateSecretsCalls < m.failUntilCall {
  60. return m.returnError
  61. }
  62. return nil
  63. }
  64. func TestRetryClientSuccessOnFirstAttempt(t *testing.T) {
  65. mock := &mockClient{
  66. failUntilCall: 1, // succeed on first call
  67. secretResponse: &client.SecretResponse{Name: "test", Value: testSecretValue},
  68. secretsResponse: &client.SecretsResponse{Secrets: client.Secrets{"test": testSecretValue}},
  69. }
  70. retryClient := newRetryableClient(mock, 3, 10*time.Millisecond)
  71. // Test Authenticate
  72. if err := retryClient.Authenticate(); err != nil {
  73. t.Errorf("Authenticate should succeed on first attempt, got error: %v", err)
  74. }
  75. if mock.authenticateCalls != 1 {
  76. t.Errorf("Expected 1 authenticate call, got %d", mock.authenticateCalls)
  77. }
  78. // Test GetSecret
  79. resp, err := retryClient.GetSecret(client.SecretRequest{Name: "test"})
  80. if err != nil {
  81. t.Errorf("GetSecret should succeed on first attempt, got error: %v", err)
  82. }
  83. if resp == nil || resp.Value != testSecretValue {
  84. t.Errorf("GetSecret returned unexpected response: %v", resp)
  85. }
  86. if mock.getSecretCalls != 1 {
  87. t.Errorf("Expected 1 getSecret call, got %d", mock.getSecretCalls)
  88. }
  89. // Test GetSecrets
  90. respSecrets, err := retryClient.GetSecrets(client.SecretsRequest{})
  91. if err != nil {
  92. t.Errorf("GetSecrets should succeed on first attempt, got error: %v", err)
  93. }
  94. if respSecrets == nil || respSecrets.Secrets["test"] != testSecretValue {
  95. t.Errorf("GetSecrets returned unexpected response: %v", respSecrets)
  96. }
  97. if mock.getSecretsCalls != 1 {
  98. t.Errorf("Expected 1 getSecrets call, got %d", mock.getSecretsCalls)
  99. }
  100. // Test UpdateSecrets
  101. if err := retryClient.UpdateSecrets(client.UpdateSecretsRequest{}); err != nil {
  102. t.Errorf("UpdateSecrets should succeed on first attempt, got error: %v", err)
  103. }
  104. if mock.updateSecretsCalls != 1 {
  105. t.Errorf("Expected 1 updateSecrets call, got %d", mock.updateSecretsCalls)
  106. }
  107. }
  108. func TestRetryClientSuccessAfterRetries(t *testing.T) {
  109. testError := errors.New("temporary error")
  110. mock := &mockClient{
  111. failUntilCall: 3, // fail twice, succeed on third attempt
  112. returnError: testError,
  113. secretResponse: &client.SecretResponse{Name: "test", Value: testSecretValue},
  114. secretsResponse: &client.SecretsResponse{Secrets: client.Secrets{"test": testSecretValue}},
  115. }
  116. retryClient := newRetryableClient(mock, 5, 1*time.Millisecond)
  117. // Test Authenticate - should retry and eventually succeed
  118. if err := retryClient.Authenticate(); err != nil {
  119. t.Errorf("Authenticate should succeed after retries, got error: %v", err)
  120. }
  121. if mock.authenticateCalls != 3 {
  122. t.Errorf("Expected 3 authenticate calls (2 failures + 1 success), got %d", mock.authenticateCalls)
  123. }
  124. // Reset for GetSecret test
  125. mock.getSecretCalls = 0
  126. resp, err := retryClient.GetSecret(client.SecretRequest{Name: "test"})
  127. if err != nil {
  128. t.Errorf("GetSecret should succeed after retries, got error: %v", err)
  129. }
  130. if resp == nil || resp.Value != testSecretValue {
  131. t.Errorf("GetSecret returned unexpected response: %v", resp)
  132. }
  133. if mock.getSecretCalls != 3 {
  134. t.Errorf("Expected 3 getSecret calls (2 failures + 1 success), got %d", mock.getSecretCalls)
  135. }
  136. }
  137. func TestRetryClientFailureAfterMaxRetries(t *testing.T) {
  138. testError := errors.New("persistent error")
  139. mock := &mockClient{
  140. failUntilCall: 100, // always fail
  141. returnError: testError,
  142. }
  143. retryClient := newRetryableClient(mock, 3, 1*time.Millisecond)
  144. // Test Authenticate - should fail after max retries
  145. err := retryClient.Authenticate()
  146. if err == nil {
  147. t.Error("Authenticate should fail after max retries")
  148. }
  149. if !errors.Is(err, testError) {
  150. t.Errorf("Expected error %v, got %v", testError, err)
  151. }
  152. // With maxRetries=3, backoff.Steps=3 means 3 total attempts
  153. if mock.authenticateCalls != 3 {
  154. t.Errorf("Expected 3 authenticate calls, got %d", mock.authenticateCalls)
  155. }
  156. // Reset for GetSecret test
  157. mock.getSecretCalls = 0
  158. _, err = retryClient.GetSecret(client.SecretRequest{Name: "test"})
  159. if err == nil {
  160. t.Error("GetSecret should fail after max retries")
  161. }
  162. if !errors.Is(err, testError) {
  163. t.Errorf("Expected error %v, got %v", testError, err)
  164. }
  165. if mock.getSecretCalls != 3 {
  166. t.Errorf("Expected 3 getSecret calls, got %d", mock.getSecretCalls)
  167. }
  168. }
  169. func TestRetryClientRetryInterval(t *testing.T) {
  170. testError := errors.New("temporary error")
  171. mock := &mockClient{
  172. failUntilCall: 3, // fail twice
  173. returnError: testError,
  174. }
  175. retryInterval := 20 * time.Millisecond
  176. retryClient := newRetryableClient(mock, 5, retryInterval)
  177. start := time.Now()
  178. _ = retryClient.Authenticate()
  179. elapsed := time.Since(start)
  180. // Should have waited at least retryInterval (for the first retry)
  181. // Note: DefaultBackoff has Factor=5.0 and Jitter=0.1, so delays increase exponentially
  182. // We just verify it took some reasonable time with retries
  183. minExpected := retryInterval
  184. if elapsed < minExpected {
  185. t.Errorf("Expected at least %v elapsed time for retries, got %v", minExpected, elapsed)
  186. }
  187. // Sanity check - with exponential backoff (Factor=5.0), shouldn't take too excessively long
  188. // First retry: ~20ms, Second retry: ~100ms (20ms * 5), Total: ~120ms + execution time
  189. maxExpected := 1 * time.Second
  190. if elapsed > maxExpected {
  191. t.Errorf("Expected less than %v elapsed time, got %v (may indicate exponential backoff issue)", maxExpected, elapsed)
  192. }
  193. }
  194. func TestRetryClientBaseURL(t *testing.T) {
  195. mock := &mockClient{}
  196. retryClient := newRetryableClient(mock, 3, 10*time.Millisecond)
  197. baseURL := retryClient.BaseURL()
  198. if baseURL == nil {
  199. t.Error("BaseURL should not be nil")
  200. }
  201. if baseURL.Host != "api.doppler.com" {
  202. t.Errorf("Expected host 'api.doppler.com', got '%s'", baseURL.Host)
  203. }
  204. }
  205. func TestRetryClientZeroRetries(t *testing.T) {
  206. testError := errors.New("error")
  207. mock := &mockClient{
  208. failUntilCall: 5, // fail multiple times
  209. returnError: testError,
  210. }
  211. // maxRetries = 0 means we don't override Steps, so it uses DefaultBackoff.Steps = 4
  212. retryClient := newRetryableClient(mock, 0, 1*time.Millisecond)
  213. err := retryClient.Authenticate()
  214. if err == nil {
  215. t.Error("Expected error with failing calls")
  216. }
  217. // With maxRetries=0, Steps is not overridden, so it uses DefaultBackoff.Steps=4
  218. if mock.authenticateCalls != 4 {
  219. t.Errorf("Expected 4 authenticate calls (DefaultBackoff.Steps), got %d", mock.authenticateCalls)
  220. }
  221. }