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 }