1package typesafe23import (4 "context"5 "errors"6 "io"7 "net/http"8 "strings"9 "testing"10 "testing/synctest"11 "time"12)1314type brokenBody struct{ closed bool }1516func (*brokenBody) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF }17func (b *brokenBody) Close() error {18 b.closed = true19 return nil20}2122func TestInterruptedResponseIsClosedAndRetried(t *testing.T) {23 body := &brokenBody{}24 calls := 025 policy := DefaultRetryPolicy()26 policy.InitialDelay, policy.MaxDelay = 0, 027 client := retryClient(t, func(request *http.Request) (*http.Response, error) {28 calls++29 response := mockResponse(200, nil)30 if calls == 1 {31 response.Body = body32 }33 return response, nil34 }, policy)35 if _, err := client.ListModels(t.Context()); err != nil || calls != 2 || !body.closed {36 t.Fatalf("calls=%d closed=%v err=%v", calls, body.closed, err)37 }38}3940type delayedBody struct{ ctx context.Context }4142func (b delayedBody) Read([]byte) (int, error) {43 <-b.ctx.Done()44 return 0, b.ctx.Err()45}46func (delayedBody) Close() error { return nil }4748func TestTimeoutIncludesResponseBody(t *testing.T) {49 synctest.Test(t, func(t *testing.T) {50 client := retryClient(t, func(request *http.Request) (*http.Response, error) {51 response := mockResponse(200, nil)52 response.Body = delayedBody{ctx: request.Context()}53 return response, nil54 }, RetryPolicy{})55 start := time.Now()56 if _, err := client.ListModels(t.Context()); !errors.Is(err, context.DeadlineExceeded) {57 t.Fatalf("body read was not canceled: %v", err)58 }59 if time.Since(start) != 10*time.Second {60 t.Fatalf("attempt timeout was not applied: %s", time.Since(start))61 }62 })63}6465func TestResponseBodyLimitDoesNotRetry(t *testing.T) {66 calls := 067 client := retryClient(t, func(request *http.Request) (*http.Response, error) {68 calls++69 response := mockResponse(200, http.Header{"X-Request-Id": {"req_large"}})70 response.Body = io.NopCloser(strings.NewReader(strings.Repeat(" ", maxResponseBytes+1)))71 return response, nil72 }, DefaultRetryPolicy())73 _, err := client.ListModels(t.Context())74 responseErr, ok := errors.AsType[*ResponseError](err)75 if !ok || responseErr.RequestID != "req_large" || calls != 1 {76 t.Fatalf("calls=%d err=%v", calls, err)77 }78}7980func TestInvalidRequestsNeverUseTransport(t *testing.T) {81 client := retryClient(t, func(request *http.Request) (*http.Response, error) {82 t.Error("invalid request reached the transport")83 return mockResponse(200, nil), nil84 }, RetryPolicy{})85 if _, err := client.SystemOne(t.Context(), SystemOneRequest{}); err == nil {86 t.Fatal("empty questions accepted")87 }88 if _, err := client.ListModels(nil); err == nil {89 t.Fatal("nil context accepted")90 }91 var zero Client92 if _, err := zero.ListModels(t.Context()); err == nil {93 t.Fatal("zero client accepted")94 }95 var absent *Client96 if _, err := absent.SystemOne(t.Context(), testRequest()); err == nil {97 t.Fatal("nil client accepted")98 }99}100101func TestCustomClientIsNotMutated(t *testing.T) {102 sentinel := errors.New("caller redirect policy")103 custom := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return sentinel }}104 client, err := NewClient(WithAPIKey("key"), WithHTTPClient(custom))105 if err != nil {106 t.Fatal(err)107 }108 if client.config.httpClient == custom || custom.CheckRedirect(nil, nil) != sentinel {109 t.Fatal("caller client was changed")110 }111}112113func TestRetryAfterDatesAndJitter(t *testing.T) {114 synctest.Test(t, func(t *testing.T) {115 policy := DefaultRetryPolicy()116 future := time.Now().Add(8 * time.Second).UTC().Format(http.TimeFormat)117 if got := retryDelay(policy, 0, http.Header{"Retry-After": {future}}); got != 8*time.Second {118 t.Fatalf("date delay=%s", got)119 }120 for range 100 {121 if got := retryDelay(policy, 0, nil); got < 375*time.Millisecond || got > 500*time.Millisecond {122 t.Fatalf("jitter delay out of bounds: %s", got)123 }124 }125 })126}