github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/auth/cache_test.go (about)

     1  package auth
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"sync"
     8  	"sync/atomic"
     9  	"testing"
    10  	"time"
    11  )
    12  
    13  func newTestCache(provider TokenProvider) *TokenCache {
    14  	return NewTokenCache(provider, nil)
    15  }
    16  
    17  func TestTokenCache_FirstCallFetches(t *testing.T) {
    18  	calls := 0
    19  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
    20  		calls++
    21  		return &AccessToken{Token: "tok-1"}, nil
    22  	})
    23  
    24  	token, err := cache.Token(context.Background(), "https://example.com")
    25  	if err != nil {
    26  		t.Fatal(err)
    27  	}
    28  	if token != "tok-1" {
    29  		t.Fatalf("got %q, want %q", token, "tok-1")
    30  	}
    31  	if calls != 1 {
    32  		t.Fatalf("expected 1 call, got %d", calls)
    33  	}
    34  }
    35  
    36  func TestTokenCache_NoExpiryNeverRefreshes(t *testing.T) {
    37  	calls := 0
    38  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
    39  		calls++
    40  		return &AccessToken{Token: "tok-forever", ExpiresAt: nil}, nil
    41  	})
    42  
    43  	for range 5 {
    44  		token, err := cache.Token(context.Background(), "https://example.com")
    45  		if err != nil {
    46  			t.Fatal(err)
    47  		}
    48  		if token != "tok-forever" {
    49  			t.Fatalf("got %q, want %q", token, "tok-forever")
    50  		}
    51  	}
    52  	if calls != 1 {
    53  		t.Fatalf("expected 1 call, got %d", calls)
    54  	}
    55  }
    56  
    57  func TestTokenCache_FreshTokenNotRefetched(t *testing.T) {
    58  	calls := 0
    59  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
    60  		calls++
    61  		exp := time.Now().Add(10 * time.Minute)
    62  		return &AccessToken{Token: "fresh", ExpiresAt: &exp}, nil
    63  	})
    64  
    65  	for range 3 {
    66  		token, err := cache.Token(context.Background(), "https://example.com")
    67  		if err != nil {
    68  			t.Fatal(err)
    69  		}
    70  		if token != "fresh" {
    71  			t.Fatalf("got %q, want %q", token, "fresh")
    72  		}
    73  	}
    74  	if calls != 1 {
    75  		t.Fatalf("expected 1 call, got %d", calls)
    76  	}
    77  }
    78  
    79  func TestTokenCache_AdvisoryRefreshSuccess(t *testing.T) {
    80  	callCount := atomic.Int32{}
    81  
    82  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
    83  		n := callCount.Add(1)
    84  		if n == 1 {
    85  			exp := time.Now().Add(60 * time.Second) // within advisory window
    86  			return &AccessToken{Token: "stale", ExpiresAt: &exp}, nil
    87  		}
    88  		exp := time.Now().Add(10 * time.Minute)
    89  		return &AccessToken{Token: "refreshed", ExpiresAt: &exp}, nil
    90  	})
    91  
    92  	// Prime the cache.
    93  	token, err := cache.Token(context.Background(), "https://example.com")
    94  	if err != nil {
    95  		t.Fatal(err)
    96  	}
    97  	if token != "stale" {
    98  		t.Fatalf("got %q, want %q", token, "stale")
    99  	}
   100  
   101  	// Second call should return stale and trigger background refresh.
   102  	token, err = cache.Token(context.Background(), "https://example.com")
   103  	if err != nil {
   104  		t.Fatal(err)
   105  	}
   106  	if token != "stale" {
   107  		t.Fatalf("got %q, want %q", token, "stale")
   108  	}
   109  
   110  	// Poll until the background refresh completes rather than sleeping a
   111  	// fixed duration, which is inherently flaky.
   112  	deadline := time.Now().Add(2 * time.Second)
   113  	for time.Now().Before(deadline) {
   114  		token, err = cache.Token(context.Background(), "https://example.com")
   115  		if err != nil {
   116  			t.Fatal(err)
   117  		}
   118  		if token == "refreshed" {
   119  			return
   120  		}
   121  		time.Sleep(1 * time.Millisecond)
   122  	}
   123  	t.Fatal("background refresh did not complete in time")
   124  }
   125  
   126  func TestTokenCache_AdvisoryRefreshFailureServesStale(t *testing.T) {
   127  	callCount := atomic.Int32{}
   128  	refreshDone := make(chan struct{})
   129  
   130  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   131  		n := callCount.Add(1)
   132  		if n == 1 {
   133  			exp := time.Now().Add(60 * time.Second)
   134  			return &AccessToken{Token: "stale", ExpiresAt: &exp}, nil
   135  		}
   136  		defer close(refreshDone)
   137  		return nil, fmt.Errorf("refresh failed")
   138  	})
   139  
   140  	// Prime.
   141  	_, _ = cache.Token(context.Background(), "https://example.com")
   142  
   143  	// Should return stale despite background refresh failure.
   144  	token, err := cache.Token(context.Background(), "https://example.com")
   145  	if err != nil {
   146  		t.Fatal(err)
   147  	}
   148  	if token != "stale" {
   149  		t.Fatalf("got %q, want %q", token, "stale")
   150  	}
   151  
   152  	<-refreshDone
   153  }
   154  
   155  func TestTokenCache_MandatoryRefreshFailureReturnsError(t *testing.T) {
   156  	callCount := atomic.Int32{}
   157  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   158  		n := callCount.Add(1)
   159  		if n == 1 {
   160  			exp := time.Now().Add(10 * time.Second) // within mandatory window
   161  			return &AccessToken{Token: "expiring", ExpiresAt: &exp}, nil
   162  		}
   163  		return nil, fmt.Errorf("refresh failed")
   164  	})
   165  
   166  	// Prime.
   167  	_, _ = cache.Token(context.Background(), "https://example.com")
   168  
   169  	// Mandatory refresh should propagate the error.
   170  	_, err := cache.Token(context.Background(), "https://example.com")
   171  	if err == nil {
   172  		t.Fatal("expected error")
   173  	}
   174  }
   175  
   176  func TestTokenCache_ExpiredIsMandatory(t *testing.T) {
   177  	callCount := atomic.Int32{}
   178  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   179  		n := callCount.Add(1)
   180  		if n == 1 {
   181  			exp := time.Now().Add(-1 * time.Second) // already expired
   182  			return &AccessToken{Token: "expired", ExpiresAt: &exp}, nil
   183  		}
   184  		exp := time.Now().Add(10 * time.Minute)
   185  		return &AccessToken{Token: "new", ExpiresAt: &exp}, nil
   186  	})
   187  
   188  	// Prime with expired token.
   189  	_, _ = cache.Token(context.Background(), "https://example.com")
   190  
   191  	token, err := cache.Token(context.Background(), "https://example.com")
   192  	if err != nil {
   193  		t.Fatal(err)
   194  	}
   195  	if token != "new" {
   196  		t.Fatalf("got %q, want %q", token, "new")
   197  	}
   198  }
   199  
   200  func TestTokenCache_Invalidate(t *testing.T) {
   201  	callCount := atomic.Int32{}
   202  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   203  		n := callCount.Add(1)
   204  		return &AccessToken{Token: fmt.Sprintf("tok-%d", n)}, nil
   205  	})
   206  
   207  	token, _ := cache.Token(context.Background(), "https://example.com")
   208  	if token != "tok-1" {
   209  		t.Fatalf("got %q, want %q", token, "tok-1")
   210  	}
   211  
   212  	cache.Invalidate()
   213  
   214  	token, _ = cache.Token(context.Background(), "https://example.com")
   215  	if token != "tok-2" {
   216  		t.Fatalf("got %q, want %q", token, "tok-2")
   217  	}
   218  }
   219  
   220  func TestTokenCache_ConcurrentFetchDeduplication(t *testing.T) {
   221  	calls := atomic.Int32{}
   222  	cache := newTestCache(func(ctx context.Context, _ string, _ func(*http.Request) (*http.Response, error)) (*AccessToken, error) {
   223  		calls.Add(1)
   224  		time.Sleep(50 * time.Millisecond)
   225  		return &AccessToken{Token: "concurrent"}, nil
   226  	})
   227  
   228  	var wg sync.WaitGroup
   229  	for range 10 {
   230  		wg.Add(1)
   231  		go func() {
   232  			defer wg.Done()
   233  			token, err := cache.Token(context.Background(), "https://example.com")
   234  			if err != nil {
   235  				t.Error(err)
   236  				return
   237  			}
   238  			if token != "concurrent" {
   239  				t.Errorf("got %q, want %q", token, "concurrent")
   240  			}
   241  		}()
   242  	}
   243  	wg.Wait()
   244  
   245  	if n := calls.Load(); n != 1 {
   246  		t.Fatalf("expected 1 provider call, got %d", n)
   247  	}
   248  }