package zarinhub import ( "context" "encoding/json" "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) } }