705 lines
26 KiB
Go
705 lines
26 KiB
Go
// Package cognitotest provides end-to-end integration tests for the Cognito authentication system.
|
|
//
|
|
// This package contains comprehensive tests that validate the complete authentication flow,
|
|
// including the critical auto-refresh token functionality. The tests use real browser automation
|
|
// via chromedp to simulate actual user interactions and test the full authentication lifecycle.
|
|
//
|
|
// Key Features Tested:
|
|
// - Full OAuth/PKCE authentication flow with AWS Cognito
|
|
// - Automatic token refresh when tokens expire
|
|
// - Cookie-based token storage and management
|
|
// - Integration with Permit.io for authorization
|
|
// - Real browser interaction using Chrome/Chromium
|
|
// - Natural token expiration testing (5-minute tokens)
|
|
//
|
|
// Test Requirements:
|
|
// - Valid AWS Cognito configuration
|
|
// - Test user credentials
|
|
// - Chrome/Chromium browser available
|
|
// - ENABLE_REFRESH_TESTS=true environment variable
|
|
//
|
|
// The tests are designed to catch regressions in the authentication system and ensure
|
|
// that users can maintain authenticated sessions without manual re-authentication.
|
|
package cognitotest
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"queryorchestration/internal/cognitoauth"
|
|
"queryorchestration/internal/serviceconfig/auth"
|
|
|
|
"github.com/chromedp/cdproto/network"
|
|
"github.com/chromedp/chromedp"
|
|
)
|
|
|
|
// setupEnvironmentForRefresh ensures all required environment variables are set for refresh testing.
|
|
//
|
|
// This function validates that all necessary Cognito configuration and test credentials
|
|
// are available in the environment. It's a prerequisite step that must pass before any
|
|
// authentication testing can begin.
|
|
//
|
|
// Required Environment Variables:
|
|
// - USER: Test user email/username for Cognito authentication
|
|
// - PASSWORD: Test user password for Cognito authentication
|
|
// - COGNITO_DOMAIN: AWS Cognito hosted UI domain URL
|
|
// - AWS_REGION: AWS region where Cognito user pool is located
|
|
// - COGNITO_CLIENT_SECRET: OAuth client secret for Cognito app client
|
|
// - COGNITO_USER_POOL_ID: Cognito user pool identifier
|
|
// - COGNITO_CLIENT_ID: Cognito app client identifier
|
|
// - HEADLESS: Optional, controls if browser runs in headless mode (defaults to "true")
|
|
// - DEBUG: Set to "false" for cleaner test output
|
|
//
|
|
// The function will fail the test immediately if any required variable is missing,
|
|
// ensuring tests don't run with incomplete configuration.
|
|
//
|
|
// Parameters:
|
|
// - t: Testing context for logging and test control
|
|
//
|
|
// Behavior:
|
|
// - Validates all required environment variables are present
|
|
// - Sets HEADLESS to "true" if not specified
|
|
// - Calls t.Fatalf() if any required variable is missing
|
|
// - Re-exports all variables to ensure they're available to subprocesses
|
|
func setupEnvironmentForRefresh(t *testing.T) {
|
|
// Define all required environment variables with test values
|
|
envVars := map[string]string{
|
|
"USER": os.Getenv("USER"),
|
|
"PASSWORD": os.Getenv("PASSWORD"),
|
|
"COGNITO_DOMAIN": os.Getenv("COGNITO_DOMAIN"),
|
|
"AWS_REGION": os.Getenv("AWS_REGION"),
|
|
"COGNITO_CLIENT_SECRET": os.Getenv("COGNITO_CLIENT_SECRET"),
|
|
"COGNITO_USER_POOL_ID": os.Getenv("COGNITO_USER_POOL_ID"),
|
|
"COGNITO_CLIENT_ID": os.Getenv("COGNITO_CLIENT_ID"),
|
|
"DEBUG": "false",
|
|
"HEADLESS": os.Getenv("HEADLESS"),
|
|
}
|
|
|
|
// Verify all required variables are set
|
|
for key, value := range envVars {
|
|
if value == "" {
|
|
// HEADLESS is optional with a default of "true"
|
|
if key == "HEADLESS" {
|
|
os.Setenv(key, "true")
|
|
continue
|
|
}
|
|
t.Fatalf("Required environment variable %s is not set", key)
|
|
}
|
|
// Set the environment variable for the test
|
|
os.Setenv(key, value)
|
|
}
|
|
}
|
|
|
|
// setupTestServerForRefresh creates and configures an Echo server for refresh testing.
|
|
//
|
|
// This function sets up a complete web server with authentication middleware that
|
|
// mimics the production environment. It's essential for testing the auto-refresh
|
|
// functionality because it provides the protected endpoints that trigger token
|
|
// refresh when accessed with expired tokens.
|
|
//
|
|
// Server Configuration:
|
|
// - Listens on localhost:8080 (matches Cognito redirect URI configuration)
|
|
// - Uses Echo web framework with logging and recovery middleware
|
|
// - Registers complete Cognito auth middleware stack
|
|
// - Configures Permit.io authorization for protected routes
|
|
// - Sets up structured logging at debug level for detailed test output
|
|
//
|
|
// Test Endpoints Created:
|
|
// - /home: Public endpoint that shows authentication status and user info
|
|
// - /query: Protected endpoint requiring "exporters", "uploaders", or "querybuilders" role
|
|
// - All Cognito auth routes: /login, /login-callback, /logout
|
|
//
|
|
// The /query endpoint is crucial for auto-refresh testing because:
|
|
// - It requires valid authentication (triggers token validation)
|
|
// - It's protected by Permit.io authorization (requires valid user context)
|
|
// - It returns a success response when auth succeeds
|
|
// - It's the endpoint we hit to trigger auto-refresh when tokens expire
|
|
//
|
|
// Parameters:
|
|
// - t: Testing context for logging and test control
|
|
//
|
|
// Returns:
|
|
// - *http.Server: Configured HTTP server ready to start
|
|
// - *auth.CognitoConfig: Auth configuration used by the server
|
|
//
|
|
// Usage:
|
|
// - Server must be started in a goroutine: go server.ListenAndServe()
|
|
// - Server should be gracefully shutdown after test: server.Shutdown(ctx)
|
|
// - Config contains auth paths needed for browser navigation
|
|
func setupTestServerForRefresh(t *testing.T) (*http.Server, *auth.CognitoConfig) {
|
|
// Create a new Echo instance
|
|
e := echo.New()
|
|
|
|
// Add basic middleware
|
|
e.Use(middleware.Logger())
|
|
e.Use(middleware.Recover())
|
|
|
|
// Initialize logger
|
|
logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
})
|
|
logger := slog.New(logHandler)
|
|
|
|
// Create auth config with correct base URL that matches Cognito settings
|
|
baseURL := "http://localhost:8080"
|
|
config := auth.InitializeConfig(baseURL, logger)
|
|
|
|
// Set route permissions based on test requirements
|
|
routePermissions := map[string][]string{
|
|
"/query": {"exporters", "uploaders", "querybuilders"},
|
|
}
|
|
config.SetAuthRoutePermissions(routePermissions)
|
|
|
|
// Register Cognito auth routes and middleware
|
|
cognitoauth.RegisterRoutes(e, config)
|
|
|
|
// Add test endpoints
|
|
e.GET("/home", func(c echo.Context) error {
|
|
// Check if user is authenticated
|
|
userInfo, authenticated := cognitoauth.GetUserInfo(c)
|
|
|
|
response := map[string]interface{}{
|
|
"authenticated": authenticated,
|
|
}
|
|
|
|
if authenticated {
|
|
response["username"] = userInfo.Username
|
|
response["email"] = userInfo.Email
|
|
response["groups"] = userInfo.Groups
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
})
|
|
|
|
e.GET("/query", func(c echo.Context) error {
|
|
return c.JSON(http.StatusOK, map[string]string{
|
|
"status": "success",
|
|
"message": "You have access to the query endpoint",
|
|
})
|
|
})
|
|
|
|
// Create server with fixed address to match Cognito config
|
|
server := &http.Server{
|
|
Addr: ":8080",
|
|
Handler: e,
|
|
ReadHeaderTimeout: 30 * time.Second,
|
|
}
|
|
|
|
// Log the redirect URL for debugging
|
|
logger.Info("Using redirect URI", "uri", config.GetAuthRedirectURI())
|
|
|
|
return server, config
|
|
}
|
|
|
|
// parseJWTTokenExpiration extracts the expiration time from a JWT token without verification.
|
|
//
|
|
// This function performs client-side JWT parsing to extract the token's expiration time
|
|
// without requiring signature verification or external dependencies. It's specifically
|
|
// designed for testing scenarios where we need to know when a token expires so we can
|
|
// wait for natural expiration and test auto-refresh functionality.
|
|
//
|
|
// JWT Structure Parsing:
|
|
// - Expects standard JWT format: header.payload.signature
|
|
// - Extracts and base64-decodes the payload (middle section)
|
|
// - Parses JSON payload to access claims
|
|
// - Specifically looks for the "exp" (expiration) claim
|
|
//
|
|
// Key Features:
|
|
// - No signature verification (testing purposes only)
|
|
// - Handles base64 URL encoding with proper padding
|
|
// - Converts Unix timestamp to Go time.Time
|
|
// - Robust error handling for malformed tokens
|
|
//
|
|
// Security Note:
|
|
// This function is for testing only and should never be used in production
|
|
// code where token signature verification is required for security.
|
|
//
|
|
// Parameters:
|
|
// - tokenString: Raw JWT token string (header.payload.signature format)
|
|
//
|
|
// Returns:
|
|
// - time.Time: Token expiration time in UTC
|
|
// - error: Error if token is malformed or missing expiration claim
|
|
//
|
|
// Error Conditions:
|
|
// - Invalid JWT format (not 3 parts separated by dots)
|
|
// - Failed base64 decoding of payload
|
|
// - Invalid JSON in payload
|
|
// - Missing "exp" claim
|
|
// - Invalid expiration value type (not a number)
|
|
//
|
|
// Usage in Testing:
|
|
// This enables the test to calculate exactly how long to wait for token
|
|
// expiration, allowing for precise timing of auto-refresh scenarios.
|
|
func parseJWTTokenExpiration(tokenString string) (time.Time, error) {
|
|
// Split JWT into parts
|
|
parts := strings.Split(tokenString, ".")
|
|
if len(parts) != 3 {
|
|
return time.Time{}, fmt.Errorf("invalid JWT format")
|
|
}
|
|
|
|
// Decode payload (second part)
|
|
payload := parts[1]
|
|
// Add padding if needed
|
|
for len(payload)%4 != 0 {
|
|
payload += "="
|
|
}
|
|
|
|
decoded, err := base64.URLEncoding.DecodeString(payload)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("failed to decode JWT payload: %v", err)
|
|
}
|
|
|
|
// Parse JSON payload
|
|
var claims map[string]interface{}
|
|
if err := json.Unmarshal(decoded, &claims); err != nil {
|
|
return time.Time{}, fmt.Errorf("failed to parse JWT claims: %v", err)
|
|
}
|
|
|
|
// Extract expiration time
|
|
exp, ok := claims["exp"]
|
|
if !ok {
|
|
return time.Time{}, fmt.Errorf("no exp claim found in JWT")
|
|
}
|
|
|
|
// Convert to time.Time
|
|
switch expValue := exp.(type) {
|
|
case float64:
|
|
return time.Unix(int64(expValue), 0), nil
|
|
default:
|
|
return time.Time{}, fmt.Errorf("invalid exp claim type")
|
|
}
|
|
}
|
|
|
|
// TestAutoRefreshFlow runs the complete end-to-end test for auto refresh functionality.
|
|
//
|
|
// This is the main integration test that validates the entire auto-refresh token lifecycle
|
|
// using real browser automation and natural token expiration. It's designed to catch
|
|
// regressions in the authentication system that could break seamless user sessions.
|
|
//
|
|
// Test Flow Overview:
|
|
// 1. Environment Setup: Validates all required configuration
|
|
// 2. Server Setup: Creates test server with auth middleware
|
|
// 3. Browser Setup: Configures Chrome with appropriate options
|
|
// 4. Initial Authentication: Complete OAuth/PKCE login flow
|
|
// 5. Token Extraction: Parses JWT to determine expiration time
|
|
// 6. Expiration Wait: Waits for natural token expiration (up to 5+ minutes)
|
|
// 7. Auto-Refresh Test: Triggers refresh by accessing protected endpoint
|
|
// 8. Verification: Confirms new token and continued access
|
|
//
|
|
// Key Testing Aspects:
|
|
// - Uses real AWS Cognito for authentication (not mocked)
|
|
// - Uses real browser (Chrome/Chromium) for authentic user experience
|
|
// - Tests natural token expiration (5-minute tokens from Cognito)
|
|
// - Validates cookie-based token storage and retrieval
|
|
// - Tests non-rotating refresh token handling
|
|
// - Verifies Permit.io authorization integration
|
|
// - Confirms seamless user experience (no re-authentication required)
|
|
//
|
|
// Browser Automation Details:
|
|
// - Supports both headless and visible browser modes
|
|
// - Handles Cognito's hosted UI authentication forms
|
|
// - Waits for OAuth redirects and callback handling
|
|
// - Captures and analyzes HTTP cookies for token storage
|
|
// - Tests protected endpoint access before and after refresh
|
|
//
|
|
// Time Requirements:
|
|
// - Test duration: 5-10 minutes (depends on token expiration time)
|
|
// - Extended timeout: 15 minutes to handle longer token lifecycles
|
|
// - Progress logging: Shows wait progress every 30 seconds
|
|
//
|
|
// Test Phases (Subtests):
|
|
// 1. "Initial Authentication": Complete login flow and cookie capture
|
|
// 2. "Extract Token and Log Expiration": JWT parsing and timing calculation
|
|
// 3. "Wait for Token Expiration": Patient wait for natural expiration
|
|
// 4. "Test Auto Refresh Functionality": Core auto-refresh validation
|
|
// 5. "Verify Continued Access After Refresh": Confirms sustained access
|
|
//
|
|
// Success Criteria:
|
|
// - User successfully authenticates with Cognito
|
|
// - JWT tokens are properly stored in HTTP cookies
|
|
// - Token expiration time is correctly parsed and tracked
|
|
// - Auto-refresh triggers when accessing protected endpoints with expired tokens
|
|
// - New tokens are generated and stored without user intervention
|
|
// - Protected endpoints remain accessible after token refresh
|
|
// - Token expiration times are extended after refresh
|
|
//
|
|
// Failure Detection:
|
|
// - Missing or invalid authentication tokens
|
|
// - Failed access to protected endpoints
|
|
// - Redirect to login page (indicates failed auto-refresh)
|
|
// - Same token before and after refresh attempt
|
|
// - Earlier expiration time after refresh attempt
|
|
//
|
|
// Environment Requirements:
|
|
// - ENABLE_REFRESH_TESTS=true (test is skipped otherwise)
|
|
// - Valid Cognito configuration (domain, client ID, secret, etc.)
|
|
// - Test user credentials (USER, PASSWORD environment variables)
|
|
// - Chrome/Chromium browser available on system
|
|
// - Network access to AWS Cognito services
|
|
//
|
|
// This test is crucial for ensuring users don't experience authentication
|
|
// interruptions during long sessions, maintaining a smooth user experience.
|
|
func TestAutoRefreshFlow(t *testing.T) {
|
|
if os.Getenv("ENABLE_REFRESH_TESTS") != "true" {
|
|
t.Skip("Skipping auto refresh tests - set ENABLE_REFRESH_TESTS=true to run")
|
|
}
|
|
|
|
// Skip if running in CI environment or short test mode
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
// Setup test environment
|
|
setupEnvironmentForRefresh(t)
|
|
|
|
// Get credentials from environment
|
|
username := os.Getenv("USER")
|
|
password := os.Getenv("PASSWORD")
|
|
|
|
// Setup test server
|
|
server, config := setupTestServerForRefresh(t)
|
|
|
|
// Start the server in a goroutine
|
|
go func() {
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
t.Logf("Server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Ensure server shuts down at the end of the test
|
|
defer func() {
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
if err := server.Shutdown(ctx); err != nil {
|
|
t.Logf("Server shutdown error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Give the server time to start
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Log important URLs for debugging
|
|
baseURL := "http://localhost:8080"
|
|
t.Logf("Server URL: %s", baseURL)
|
|
t.Logf("Login URL: %s%s", baseURL, config.GetAuthLoginPath())
|
|
t.Logf("Callback URL: %s", config.GetAuthRedirectURI())
|
|
|
|
// Get headless mode from environment variable
|
|
headless := true
|
|
if os.Getenv("HEADLESS") == "false" {
|
|
headless = false
|
|
}
|
|
|
|
// Setup Chrome options
|
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
|
chromedp.Flag("headless", headless),
|
|
chromedp.Flag("disable-gpu", true),
|
|
chromedp.Flag("no-sandbox", true),
|
|
chromedp.Flag("disable-dev-shm-usage", true),
|
|
chromedp.Flag("host-blocking-patterns", "*prometheus*"),
|
|
chromedp.Flag("disable-cache", true),
|
|
)
|
|
|
|
allocCtx, cancel := chromedp.NewExecAllocator(t.Context(), opts...)
|
|
defer cancel()
|
|
|
|
// Create Chrome context with extended timeout for token expiration testing
|
|
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
|
|
defer cancel()
|
|
ctx, cancel = context.WithTimeout(ctx, 15*time.Minute) // Extended timeout for token expiration
|
|
defer cancel()
|
|
|
|
// Enable request interception for debugging
|
|
if err := chromedp.Run(ctx, network.Enable()); err != nil {
|
|
t.Fatalf("Failed to enable network: %v", err)
|
|
}
|
|
|
|
// Store cookies to extract JWT tokens
|
|
var cookies []*network.Cookie
|
|
|
|
// Phase 1: Initial Authentication
|
|
// This phase performs the complete OAuth/PKCE authentication flow using the browser.
|
|
// It navigates to the Cognito hosted UI, submits credentials, handles redirects,
|
|
// and captures authentication cookies containing JWT tokens.
|
|
t.Run("Initial Authentication", func(t *testing.T) {
|
|
// Navigate to login page
|
|
loginURL := fmt.Sprintf("%s%s", baseURL, config.GetAuthLoginPath())
|
|
err := chromedp.Run(ctx,
|
|
chromedp.Navigate(loginURL),
|
|
chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery),
|
|
)
|
|
assert.NoError(t, err, "Failed to navigate to login page")
|
|
|
|
// Submit login credentials
|
|
err = chromedp.Run(ctx,
|
|
chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery),
|
|
chromedp.Clear(`input[name="username"]`, chromedp.ByQuery),
|
|
chromedp.SendKeys(`input[name="username"]`, username, chromedp.ByQuery),
|
|
chromedp.Click(`button[type="submit"], button.amplify-button--primary, input[type="Submit"]`, chromedp.ByQuery),
|
|
)
|
|
assert.NoError(t, err, "Failed to submit username")
|
|
|
|
// Enter password
|
|
err = chromedp.Run(ctx,
|
|
chromedp.WaitVisible(`input[name="password"]`, chromedp.ByQuery),
|
|
chromedp.Clear(`input[name="password"]`, chromedp.ByQuery),
|
|
chromedp.SendKeys(`input[name="password"]`, password, chromedp.ByQuery),
|
|
chromedp.Sleep(1*time.Second),
|
|
chromedp.Click(`button[type="submit"], button.amplify-button--primary, input[type="Submit"]`, chromedp.ByQuery),
|
|
)
|
|
assert.NoError(t, err, "Failed to submit password")
|
|
|
|
// Wait for redirection to home page
|
|
err = chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
|
|
startTime := time.Now()
|
|
timeout := 45 * time.Second
|
|
|
|
for {
|
|
if time.Since(startTime) > timeout {
|
|
return fmt.Errorf("timed out waiting for redirect")
|
|
}
|
|
|
|
var currentURL string
|
|
if err := chromedp.Location(¤tURL).Do(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.Contains(currentURL, "/home") {
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(currentURL, "/login-callback") {
|
|
time.Sleep(2 * time.Second)
|
|
continue
|
|
}
|
|
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}))
|
|
assert.NoError(t, err, "Failed to wait for redirect after login")
|
|
|
|
// Get cookies
|
|
err = chromedp.Run(ctx,
|
|
chromedp.Sleep(2*time.Second),
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
cookies, err = network.GetCookies().Do(ctx)
|
|
return err
|
|
}),
|
|
)
|
|
assert.NoError(t, err, "Failed to get cookies")
|
|
|
|
t.Log("✅ Initial authentication completed successfully")
|
|
})
|
|
|
|
// Phase 2: Extract and Log Token Expiration
|
|
// This phase extracts the JWT access token from cookies and parses it to determine
|
|
// the exact expiration time. This information is crucial for calculating how long
|
|
// to wait before testing the auto-refresh functionality.
|
|
var authToken string
|
|
var tokenExpiration time.Time
|
|
t.Run("Extract Token and Log Expiration", func(t *testing.T) {
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "auth_token" {
|
|
authToken = cookie.Value
|
|
break
|
|
}
|
|
}
|
|
assert.NotEmpty(t, authToken, "JWT token cookie not found")
|
|
|
|
var err error
|
|
tokenExpiration, err = parseJWTTokenExpiration(authToken)
|
|
assert.NoError(t, err, "Failed to parse token expiration")
|
|
|
|
t.Logf("✅ Token extracted successfully")
|
|
t.Logf("📅 Token expires at: %s", tokenExpiration.Format(time.RFC3339))
|
|
t.Logf("⏰ Current time: %s", time.Now().Format(time.RFC3339))
|
|
t.Logf("⌛ Time until expiration: %s", time.Until(tokenExpiration))
|
|
t.Logf("🔄 Auto refresh should trigger when token has < 5 minutes remaining")
|
|
})
|
|
|
|
// Phase 3: Wait for Token Expiration
|
|
// This phase waits for the natural expiration of the JWT access token.
|
|
// It calculates the exact wait time and provides progress updates during the wait.
|
|
// This is essential for testing real-world token expiration scenarios.
|
|
t.Run("Wait for Token Expiration", func(t *testing.T) {
|
|
// Calculate when the token will expire (plus 1 second buffer)
|
|
timeToWait := time.Until(tokenExpiration) + 1*time.Second
|
|
|
|
// If token expires in more than 10 minutes, something might be wrong
|
|
if timeToWait > 10*time.Minute {
|
|
t.Logf("⚠️ Token expires in %s, which seems longer than expected for a 5-minute token", timeToWait)
|
|
t.Logf("🤔 Proceeding anyway, but you may want to check Cognito token settings")
|
|
}
|
|
|
|
if timeToWait > 0 {
|
|
t.Logf("⏳ Waiting %s for token to expire...", timeToWait)
|
|
|
|
// Wait in chunks and log progress
|
|
start := time.Now()
|
|
for timeToWait > 0 {
|
|
if timeToWait > 30*time.Second {
|
|
time.Sleep(30 * time.Second)
|
|
timeToWait = time.Until(tokenExpiration) + 1*time.Second
|
|
elapsed := time.Since(start)
|
|
t.Logf("⏳ Still waiting... elapsed: %s, remaining: %s", elapsed.Round(time.Second), timeToWait.Round(time.Second))
|
|
} else {
|
|
time.Sleep(timeToWait)
|
|
timeToWait = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Logf("✅ Wait period completed - token should now be expired")
|
|
t.Logf("⏰ Current time: %s", time.Now().Format(time.RFC3339))
|
|
})
|
|
|
|
// Phase 4: Test Auto Refresh
|
|
// This is the core phase that tests the auto-refresh functionality.
|
|
// It attempts to access a protected endpoint with an expired token, expecting
|
|
// the middleware to automatically refresh the token and grant access.
|
|
t.Run("Test Auto Refresh Functionality", func(t *testing.T) {
|
|
// Get cookies before the request
|
|
var cookiesBefore []*network.Cookie
|
|
err := chromedp.Run(ctx,
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
var err error
|
|
cookiesBefore, err = network.GetCookies().Do(ctx)
|
|
return err
|
|
}),
|
|
)
|
|
assert.NoError(t, err, "Failed to get cookies before refresh test")
|
|
|
|
// Log all cookies for debugging
|
|
cookieNames := make([]string, 0, len(cookiesBefore))
|
|
for _, cookie := range cookiesBefore {
|
|
cookieNames = append(cookieNames, cookie.Name)
|
|
}
|
|
t.Logf("🍪 Cookies before refresh test: %v", cookieNames)
|
|
|
|
var originalAuthToken string
|
|
for _, cookie := range cookiesBefore {
|
|
if cookie.Name == "auth_token" {
|
|
originalAuthToken = cookie.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
if originalAuthToken == "" {
|
|
t.Logf("⚠️ No auth token found in cookies before refresh test")
|
|
} else {
|
|
t.Logf("🔍 Original token (first 20 chars): %s...", originalAuthToken[:20])
|
|
}
|
|
|
|
// Make a request to a protected endpoint to trigger auto refresh
|
|
queryURL := fmt.Sprintf("%s/query", baseURL)
|
|
var queryResponseBody string
|
|
|
|
t.Logf("🚀 Making request to %s to trigger auto refresh...", queryURL)
|
|
|
|
err = chromedp.Run(ctx,
|
|
chromedp.Navigate(queryURL),
|
|
chromedp.Sleep(5*time.Second), // Give time for auto refresh to complete
|
|
chromedp.OuterHTML("body", &queryResponseBody),
|
|
)
|
|
assert.NoError(t, err, "Failed to access protected endpoint")
|
|
|
|
// Get cookies after the request
|
|
var cookiesAfter []*network.Cookie
|
|
err = chromedp.Run(ctx,
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
var err error
|
|
cookiesAfter, err = network.GetCookies().Do(ctx)
|
|
return err
|
|
}),
|
|
)
|
|
assert.NoError(t, err, "Failed to get cookies after refresh test")
|
|
|
|
// Log all cookies after refresh for debugging
|
|
cookieNamesAfter := make([]string, 0, len(cookiesAfter))
|
|
for _, cookie := range cookiesAfter {
|
|
cookieNamesAfter = append(cookieNamesAfter, cookie.Name)
|
|
}
|
|
t.Logf("🍪 Cookies after refresh test: %v", cookieNamesAfter)
|
|
|
|
// Log the response body for debugging
|
|
t.Logf("📄 Response body: %s", queryResponseBody)
|
|
|
|
// Check if we got a successful response (not a redirect to login)
|
|
assert.Contains(t, queryResponseBody, "success", "Auto refresh should have succeeded and allowed access to protected endpoint")
|
|
|
|
// Find the new auth token
|
|
var newAuthToken string
|
|
for _, cookie := range cookiesAfter {
|
|
if cookie.Name == "auth_token" {
|
|
newAuthToken = cookie.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.NotEmpty(t, newAuthToken, "Auth token should be present after successful auto refresh")
|
|
|
|
if newAuthToken != "" {
|
|
// Verify the token was refreshed (tokens should be different)
|
|
if originalAuthToken == newAuthToken {
|
|
t.Logf("⚠️ Token appears to be the same - auto refresh may not have occurred")
|
|
t.Logf("🤔 This could happen if the token wasn't actually expired yet")
|
|
} else {
|
|
t.Logf("✅ Token was refreshed successfully!")
|
|
if len(newAuthToken) >= 20 {
|
|
t.Logf("🔍 New token (first 20 chars): %s...", newAuthToken[:20])
|
|
} else {
|
|
t.Logf("🔍 New token: %s", newAuthToken)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse new token expiration
|
|
if newAuthToken != "" {
|
|
newExpiration, err := parseJWTTokenExpiration(newAuthToken)
|
|
if err == nil {
|
|
t.Logf("📅 New token expires at: %s", newExpiration.Format(time.RFC3339))
|
|
t.Logf("⌛ New token valid for: %s", time.Until(newExpiration))
|
|
|
|
// Verify new token has a later expiration than original
|
|
if newExpiration.After(tokenExpiration) {
|
|
t.Logf("✅ New token expiration is later than original - auto refresh confirmed!")
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Logf("🎉 Auto refresh test completed successfully!")
|
|
})
|
|
|
|
// Phase 5: Verify Continued Access to Protected Endpoint
|
|
// This final phase confirms that the auto-refreshed token works for subsequent
|
|
// requests, ensuring sustained access to protected resources without user intervention.
|
|
t.Run("Verify Continued Access After Refresh", func(t *testing.T) {
|
|
// Make another request to a protected endpoint to ensure the refreshed token works
|
|
queryURL := fmt.Sprintf("%s/query", baseURL)
|
|
var secondResponseBody string
|
|
|
|
err := chromedp.Run(ctx,
|
|
chromedp.Navigate(queryURL),
|
|
chromedp.Sleep(3*time.Second),
|
|
chromedp.OuterHTML("body", &secondResponseBody),
|
|
)
|
|
assert.NoError(t, err, "Failed to access protected endpoint after refresh")
|
|
|
|
assert.Contains(t, secondResponseBody, "success", "Should continue to have access to protected endpoints after auto refresh")
|
|
t.Logf("✅ Continued access to protected endpoint verified")
|
|
})
|
|
} |