package zarinhub import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "sync" "testing" "time" ) func TestTokenManagerAuthenticatesAndCachesToken(t *testing.T) { var calls int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ var request tokenRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) } if request.Username != "test-username" || request.Password != "test-password" { t.Errorf("credentials = %q/%q", request.Username, request.Password) } if request.RefreshToken != "" { t.Errorf("refresh token = %q, want empty", request.RefreshToken) } writeTokenResponse(t, w, "access-1", "refresh-1", 28800) })) defer server.Close() manager := NewTokenManager( server.Client(), server.URL, "test-username", "test-password", ) for range 2 { token, err := manager.AccessToken(context.Background()) if err != nil { t.Fatalf("AccessToken() error = %v", err) } if token != "access-1" { t.Errorf("token = %q, want access-1", token) } } if calls != 1 { t.Errorf("authentication calls = %d, want 1", calls) } } func TestTokenManagerRefreshesExpiringToken(t *testing.T) { var calls int currentTime := time.Date(2026, 9, 21, 12, 0, 0, 0, time.UTC) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ var request tokenRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) } if calls == 1 { writeTokenResponse(t, w, "access-1", "refresh-1", 120) return } if request.RefreshToken != "refresh-1" { t.Errorf("refresh token = %q, want refresh-1", request.RefreshToken) } if request.Username != "" || request.Password != "" { t.Error("refresh request included credentials") } writeTokenResponse(t, w, "access-2", "refresh-2", 28800) })) defer server.Close() manager := NewTokenManager(server.Client(), server.URL, "test-username", "test-password") manager.now = func() time.Time { return currentTime } if _, err := manager.AccessToken(context.Background()); err != nil { t.Fatalf("initial AccessToken() error = %v", err) } currentTime = currentTime.Add(61 * time.Second) token, err := manager.AccessToken(context.Background()) if err != nil { t.Fatalf("refreshed AccessToken() error = %v", err) } if token != "access-2" { t.Errorf("token = %q, want access-2", token) } } func TestTokenManagerFallsBackToCredentialsWhenRefreshIsRejected(t *testing.T) { var calls int currentTime := time.Date(2026, 9, 21, 12, 0, 0, 0, time.UTC) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ var request tokenRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) } switch calls { case 1: writeTokenResponse(t, w, "access-1", "refresh-1", 61) case 2: if request.RefreshToken != "refresh-1" { t.Errorf("refresh token = %q, want refresh-1", request.RefreshToken) } w.WriteHeader(http.StatusUnauthorized) case 3: if request.Username != "test-username" || request.Password != "test-password" { t.Error("credential fallback did not include API credentials") } writeTokenResponse(t, w, "access-2", "refresh-2", 28800) default: t.Errorf("unexpected authentication call %d", calls) } })) defer server.Close() manager := NewTokenManager(server.Client(), server.URL, "test-username", "test-password") manager.now = func() time.Time { return currentTime } if _, err := manager.AccessToken(context.Background()); err != nil { t.Fatalf("initial AccessToken() error = %v", err) } currentTime = currentTime.Add(2 * time.Second) token, err := manager.AccessToken(context.Background()) if err != nil { t.Fatalf("fallback AccessToken() error = %v", err) } if token != "access-2" { t.Errorf("token = %q, want access-2", token) } if calls != 3 { t.Errorf("authentication calls = %d, want 3", calls) } } func TestTokenManagerSharesConcurrentAuthentication(t *testing.T) { var ( mu sync.Mutex calls int ) release := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() calls++ mu.Unlock() <-release writeTokenResponse(t, w, "shared-access", "shared-refresh", 28800) })) defer server.Close() manager := NewTokenManager(server.Client(), server.URL, "test-username", "test-password") results := make(chan string, 5) errors := make(chan error, 5) for range 5 { go func() { token, err := manager.AccessToken(context.Background()) results <- token errors <- err }() } time.Sleep(20 * time.Millisecond) close(release) for range 5 { if err := <-errors; err != nil { t.Errorf("AccessToken() error = %v", err) } if token := <-results; token != "shared-access" { t.Errorf("token = %q, want shared-access", token) } } mu.Lock() defer mu.Unlock() if calls != 1 { t.Errorf("authentication calls = %d, want 1", calls) } } func writeTokenResponse( t *testing.T, w http.ResponseWriter, accessToken string, refreshToken string, expiresIn int, ) { t.Helper() w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ "accessToken": accessToken, "tokenType": "Bearer", "expiresTn": expiresIn, "refreshToken": refreshToken, }, "meta": map[string]any{ "isSuccess": true, }, }) if err != nil { t.Fatalf("encode response: %v", err) } } func TestTokenManagerRetriesOnTransientErrorsUntilSuccess(t *testing.T) { var calls int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ if calls == 1 { w.WriteHeader(http.StatusServiceUnavailable) return } writeTokenResponse(t, w, "retried-access", "retried-refresh", 28800) })) defer server.Close() var recordedDelays []time.Duration transport := NewRetryTransport( server.Client().Transport, time.Second, 2, ) transport.retryWait = func(ctx context.Context, delay time.Duration) bool { recordedDelays = append(recordedDelays, delay) return true } httpClient := &http.Client{Transport: transport} manager := NewTokenManager(httpClient, server.URL, "test-user", "test-pass") token, err := manager.AccessToken(context.Background()) if err != nil { t.Fatalf("AccessToken() error = %v", err) } if token != "retried-access" { t.Errorf("token = %q, want retried-access", token) } if calls != 2 { t.Errorf("calls = %d, want 2", calls) } if len(recordedDelays) != 1 { t.Fatalf("len(recordedDelays) = %d, want 1", len(recordedDelays)) } if recordedDelays[0] != 200*time.Millisecond { t.Errorf("delay[0] = %v, want 200ms", recordedDelays[0]) } } func TestTokenManagerRetriesOnTimeoutUntilSuccess(t *testing.T) { var calls int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ if calls == 1 { time.Sleep(150 * time.Millisecond) w.WriteHeader(http.StatusOK) return } writeTokenResponse(t, w, "timeout-recovered-access", "timeout-recovered-refresh", 28800) })) defer server.Close() var recordedDelays []time.Duration transport := NewRetryTransport( server.Client().Transport, 50*time.Millisecond, 2, ) transport.retryWait = func(ctx context.Context, delay time.Duration) bool { recordedDelays = append(recordedDelays, delay) return true } httpClient := &http.Client{Transport: transport} manager := NewTokenManager(httpClient, server.URL, "test-user", "test-pass") token, err := manager.AccessToken(context.Background()) if err != nil { t.Fatalf("AccessToken() error = %v", err) } if token != "timeout-recovered-access" { t.Errorf("token = %q, want timeout-recovered-access", token) } if calls != 2 { t.Errorf("calls = %d, want 2", calls) } if len(recordedDelays) != 1 { t.Fatalf("len(recordedDelays) = %d, want 1", len(recordedDelays)) } } func TestTokenManagerRetriesExhausted(t *testing.T) { var calls int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ w.WriteHeader(http.StatusBadGateway) })) defer server.Close() var recordedDelays []time.Duration transport := NewRetryTransport( server.Client().Transport, time.Second, 2, ) transport.retryWait = func(ctx context.Context, delay time.Duration) bool { recordedDelays = append(recordedDelays, delay) return true } httpClient := &http.Client{Transport: transport} manager := NewTokenManager(httpClient, server.URL, "test-user", "test-pass") _, err := manager.AccessToken(context.Background()) if !errors.Is(err, ErrUnavailable) { t.Fatalf("error = %v, want ErrUnavailable", err) } if calls != 3 { t.Errorf("calls = %d, want 3", calls) } if len(recordedDelays) != 2 { t.Fatalf("len(recordedDelays) = %d, want 2", len(recordedDelays)) } }