@@ -2,14 +2,17 @@ package utils
22
33import (
44 "context"
5+ "crypto/tls"
56 "fmt"
67 "net/http"
8+ "net/http/httptest"
79 "os"
810 "reflect"
911 "testing"
1012 "testing/synctest"
1113 "time"
1214
15+ "github.com/google/uuid"
1316 "github.com/hashicorp/terraform-plugin-framework/diag"
1417 sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients"
1518 "github.com/stackitcloud/stackit-sdk-go/core/config"
@@ -33,6 +36,10 @@ func TestConfigureClient(t *testing.T) {
3336 t .Errorf ("error setting env variable: %v" , err )
3437 }
3538
39+ var roundTripper http.RoundTripper = & http.Transport {
40+ TLSClientConfig : & tls.Config {MinVersion : tls .VersionTLS13 },
41+ }
42+
3643 type args struct {
3744 providerData * core.ProviderData
3845 }
@@ -46,13 +53,21 @@ func TestConfigureClient(t *testing.T) {
4653 name : "default endpoint" ,
4754 args : args {
4855 providerData : & core.ProviderData {
49- Version : testVersion ,
56+ Version : testVersion ,
57+ RoundTripper : roundTripper ,
5058 },
5159 },
5260 expected : func () * objectstorage.APIClient {
5361 apiClient , err := objectstorage .NewAPIClient (
5462 utils .UserAgentConfigOption (testVersion ),
63+ config .WithCustomAuth (& RetryTransport {
64+ Base : roundTripper ,
65+ MaxRetries : 3 ,
66+ BaseBackoff : 10 * time .Second ,
67+ MaxJitter : 500 * time .Millisecond ,
68+ }),
5569 )
70+
5671 if err != nil {
5772 t .Errorf ("error configuring client: %v" , err )
5873 }
@@ -65,13 +80,20 @@ func TestConfigureClient(t *testing.T) {
6580 args : args {
6681 providerData : & core.ProviderData {
6782 Version : testVersion ,
83+ RoundTripper : roundTripper ,
6884 ObjectStorageCustomEndpoint : testCustomEndpoint ,
6985 },
7086 },
7187 expected : func () * objectstorage.APIClient {
7288 apiClient , err := objectstorage .NewAPIClient (
7389 utils .UserAgentConfigOption (testVersion ),
7490 config .WithEndpoint (testCustomEndpoint ),
91+ config .WithCustomAuth (& RetryTransport {
92+ Base : roundTripper ,
93+ MaxRetries : 3 ,
94+ BaseBackoff : 10 * time .Second ,
95+ MaxJitter : 500 * time .Millisecond ,
96+ }),
7597 )
7698 if err != nil {
7799 t .Errorf ("error configuring client: %v" , err )
@@ -98,6 +120,81 @@ func TestConfigureClient(t *testing.T) {
98120 }
99121}
100122
123+ func TestClientRetry (t * testing.T ) {
124+ ctx := context .Background ()
125+ diags := diag.Diagnostics {}
126+
127+ testProjectId := uuid .New ().String ()
128+ const testRegion = "eu01"
129+ const testBucketName = "karl-otto"
130+
131+ attempts := 0
132+
133+ // Create mock server returning HTTP 429 on first & second call, HTTP 200 on final retry
134+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
135+ attempts ++
136+
137+ if r .URL .Path != fmt .Sprintf ("/v2/project/%s/regions/%s/bucket/%s" , testProjectId , testRegion , testBucketName ) {
138+ t .Fatalf ("invalid endpoint called" )
139+ }
140+
141+ // first request: HTTP 429 *with* Retry-After header
142+ if attempts == 1 {
143+ w .Header ().Set ("Retry-After" , "1" )
144+ w .Header ().Set ("Content-Type" , "application/json" )
145+ w .WriteHeader (http .StatusTooManyRequests )
146+ _ , err := w .Write ([]byte (`{"error": "rate_limit_exceeded"}` ))
147+ if err != nil {
148+ t .Fatalf ("error writing response: %v" , err )
149+ }
150+ return
151+ }
152+
153+ // second request: HTTP 429 *without* Retry-After header (we expect base backoff to be used now)
154+ if attempts == 2 {
155+ w .Header ().Set ("Content-Type" , "application/json" )
156+ w .WriteHeader (http .StatusTooManyRequests )
157+ _ , err := w .Write ([]byte (`{"error": "rate_limit_exceeded"}` ))
158+ if err != nil {
159+ t .Fatalf ("error writing response: %v" , err )
160+ }
161+ return
162+ }
163+
164+ w .Header ().Set ("Content-Type" , "application/json" )
165+ w .WriteHeader (http .StatusOK )
166+ _ , err := w .Write ([]byte (`{
167+ "bucket": {
168+ "name": "bucket-1",
169+ "objectLockEnabled": false,
170+ "region": "eu01",
171+ "urlPathStyle": "https://object.storage.eu01.onstackit.cloud/bucket-1",
172+ "urlVirtualHostedStyle": "https://bucket-1.object.storage.eu01.onstackit.cloud"
173+ },
174+ "project": "` + testProjectId + `"}` ))
175+ if err != nil {
176+ t .Fatalf ("error writing response: %v" , err )
177+ }
178+ }))
179+ defer server .Close ()
180+
181+ client := ConfigureClient (ctx , & core.ProviderData {
182+ ObjectStorageCustomEndpoint : server .URL ,
183+ }, & diags )
184+ if diags .HasError () {
185+ t .Fatalf ("error configuring client: %v" , diags )
186+ }
187+
188+ _ , err := client .DefaultAPI .GetBucket (ctx , testProjectId , testRegion , testBucketName ).Execute ()
189+ if err != nil {
190+ t .Fatalf ("unexpected request error: %v" , err )
191+ }
192+
193+ if attempts != 3 {
194+ t .Fatalf ("expected 3 attempts, got %d" , attempts )
195+ }
196+ }
197+
101198func TestEnableProject (t * testing.T ) {
102199 tests := []struct {
103200 description string
0 commit comments