Перейти к содержанию

JWT Testing Guide

JWT Helper CLI Utility

Installation and Build

Build JWT Helper:

# Using Makefile (recommended)
make jwt-build

# Manual build
cd backend && go build -o bin/jwt-helper cmd/jwt-helper/main.go

Verify Installation:

# Check binary exists
ls -la backend/bin/jwt-helper

# Test help command
cd backend && ./bin/jwt-helper --help

Basic Usage

Generate Admin Token:

# Using Makefile (recommended - includes detailed output)
make jwt-admin EMAIL=admin@saga-test.com

# Output:
 Admin JWT Token Generated:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
📧 Email: admin@saga-test.com
 Expires: 2025-10-07 11:00:00 UTC (24 hours)
🔑 Issuer: saga-defi-platform

Generate Admin Token (Token Only):

# Using Makefile (clean output - perfect for scripts)
make jwt-admin-token EMAIL=admin@saga-test.com

# Output (только токен):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Generate User Token:

# Using Makefile (requires database with existing user)
make jwt-user EMAIL=user@saga-test.com

# Output:
 User JWT Token Generated:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
📧 Email: user@saga-test.com
👤 User ID: 123
💼 Wallet: 0x742d35Cc9Cf3C4C3a3F5d7B5f
 Expires: 2025-10-07 11:00:00 UTC

Validate Token:

# Using Makefile
make jwt-validate TOKEN=<jwt_token>

# Direct CLI
cd backend && ./bin/jwt-helper validate <jwt_token>

# Output:
 Token Valid
📧 Email: admin@saga-test.com
 Issued: 2025-10-06 11:00:00 UTC
 Expires: 2025-10-07 11:00:00 UTC
🔑 Issuer: saga-defi-platform

Introspect Token (Detailed Analysis):

cd backend && ./bin/jwt-helper introspect <jwt_token>

# Output:
{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "email": "admin@saga-test.com",
    "permissions": [
      "approve_withdrawals",
      "view_all_users"
    ],
    "exp": 1696531200,
    "iat": 1696444800,
    "iss": "saga-defi-platform"
  },
  "signature": {
    "valid": true,
    "algorithm": "HS256"
  }
}

Test Admin API Endpoints

Test Admin Ping:

# Generate token and test in one command
TOKEN=$(make jwt-admin-token EMAIL=admin@saga-test.com) && \
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/admin/ping

# Output:
{
  "message": "Admin pong",
  "admin_email": "admin@saga-test.com",
  "timestamp": "2025-10-06T11:00:00Z"
}

Test Admin Users List:

cd backend && ./bin/jwt-helper test-admin-api <admin_token>

# Automated test of:
# - GET /api/admin/users
# - GET /api/admin/investments
# - GET /api/admin/withdrawals

Test User API Endpoints

Test User Balance:

# Generate user token
TOKEN=$(make jwt-user-token EMAIL=user@saga-test.com) && \
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/user/balance

# Output:
{
  "balance": "1000.00",
  "currency": "USDC",
  "user_id": 123
}

Test User Endpoints:

cd backend && ./bin/jwt-helper test-user-api <user_token>

# Automated test of:
# - GET /api/user/balance
# - GET /api/user/investments
# - GET /api/user/transactions

Unit Testing JWT Services

Test JWT Generation

Actual Test Files: - backend/tests/integration/admin_jwt_integration_test.go - admin JWT tests - backend/tests/unit/jwt_validation_test.go - JWT validation logic

Example Test Pattern:

func TestGenerateAdminToken(t *testing.T) {
    cfg := config.LoadTestConfig()
    jwtService := jwt_service.NewJWTService(cfg)

    // Generate admin token
    token, err := jwtService.CreateAdminToken("admin@saga-test.com")
    require.NoError(t, err)
    require.NotEmpty(t, token)

    // Parse token
    parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
        return []byte(cfg.GetJWTSecret()), nil
    })
    require.NoError(t, err)

    // Verify claims
    claims := parsedToken.Claims.(jwt.MapClaims)
    assert.Equal(t, "admin@saga-test.com", claims["email"])
    assert.NotNil(t, claims["permissions"])
    assert.Equal(t, cfg.GetJWTIssuer(), claims["iss"])
}

Test User Token Generation:

func TestGenerateUserToken(t *testing.T) {
    cfg := config.LoadTestConfig()
    jwtService := jwt_service.NewJWTService(cfg)

    // Generate user token
    token, err := jwtService.CreateUserToken(123, "user@saga-test.com", "0x742d35...")
    require.NoError(t, err)

    // Parse and verify
    parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
        return []byte(cfg.GetJWTSecret()), nil
    })
    require.NoError(t, err)

    claims := parsedToken.Claims.(jwt.MapClaims)
    assert.Equal(t, float64(123), claims["user_id"])
    assert.Equal(t, "user@saga-test.com", claims["email"])
    assert.Equal(t, "0x742d35...", claims["wallet_address"])
}

Test JWT Validation

Actual Test Files: - backend/tests/unit/jwt_validation_test.go - core validation logic - backend/tests/security/unified_jwt_validation_test.go - security validation

Example Test Pattern:

func TestValidateToken(t *testing.T) {
    cfg := config.LoadTestConfig()
    validator := jwt_validator.NewJWTValidator(cfg)
    jwtService := jwt_service.NewJWTService(cfg)

    // Create valid token
    validToken, _ := jwtService.CreateAdminToken("admin@saga-test.com")

    // Test validation
    token, err := validator.ValidateToken(validToken)
    assert.NoError(t, err)
    assert.True(t, token.Valid)
}

func TestValidateExpiredToken(t *testing.T) {
    cfg := config.LoadTestConfig()
    validator := jwt_validator.NewJWTValidator(cfg)

    // Create token with 1-second expiration
    claims := jwt_service.AdminClaims{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(1 * time.Second).Unix(),
            IssuedAt:  time.Now().Unix(),
            Issuer:    cfg.GetJWTIssuer(),
        },
        Email: "admin@saga-test.com",
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, _ := token.SignedString([]byte(cfg.GetJWTSecret()))

    // Wait for expiration
    time.Sleep(2 * time.Second)

    // Validate (should fail)
    _, err := validator.ValidateToken(tokenString)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "expired")
}

Test Signature Verification

func TestInvalidSignature(t *testing.T) {
    cfg := config.LoadTestConfig()
    validator := jwt_validator.NewJWTValidator(cfg)

    // Create token with wrong secret
    claims := jwt_service.AdminClaims{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
            IssuedAt:  time.Now().Unix(),
        },
        Email: "admin@saga-test.com",
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, _ := token.SignedString([]byte("wrong-secret"))

    // Validate (should fail)
    _, err := validator.ValidateToken(tokenString)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "signature")
}

Integration Testing with Middleware

Test Admin Auth Middleware

Actual Test Files: - backend/tests/integration/jwt_middleware_direct_test.go - middleware integration tests - backend/tests/integration/admin_jwt_integration_test.go - admin auth tests

Example Test Pattern:

func TestAdminAuthMiddleware(t *testing.T) {
    cfg := config.LoadTestConfig()
    jwtService := jwt_service.NewJWTService(cfg)

    // Create admin token
    adminToken, err := jwtService.CreateAdminToken("admin@saga-test.com")
    require.NoError(t, err)

    // Create test handler
    testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        adminEmail := r.Context().Value("admin_email").(string)
        assert.Equal(t, "admin@saga-test.com", adminEmail)
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    // Wrap with middleware
    handler := middleware.RequireAdminAuth(cfg)(testHandler)

    // Test request
    req := httptest.NewRequest("GET", "/api/admin/users", nil)
    req.Header.Set("Authorization", "Bearer "+adminToken)

    rr := httptest.NewRecorder()
    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusOK, rr.Code)
}

Test Unauthorized Access:

func TestAdminAuthMiddleware_Unauthorized(t *testing.T) {
    cfg := config.LoadTestConfig()

    testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t.Fatal("Should not reach handler")
    })

    handler := middleware.RequireAdminAuth(cfg)(testHandler)

    // Test without token
    req := httptest.NewRequest("GET", "/api/admin/users", nil)
    rr := httptest.NewRecorder()

    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusUnauthorized, rr.Code)
}

Test Non-Admin User:

func TestAdminAuthMiddleware_NonAdmin(t *testing.T) {
    cfg := config.LoadTestConfig()
    jwtService := jwt_service.NewJWTService(cfg)

    // Create user token (not admin)
    userToken, _ := jwtService.CreateUserToken(123, "user@saga-test.com", "0x...")

    testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t.Fatal("Should not reach handler")
    })

    handler := middleware.RequireAdminAuth(cfg)(testHandler)

    req := httptest.NewRequest("GET", "/api/admin/users", nil)
    req.Header.Set("Authorization", "Bearer "+userToken)

    rr := httptest.NewRecorder()
    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusForbidden, rr.Code)
}

Test User Auth Middleware

Actual Test Files: - backend/tests/integration/jwt_middleware_direct_test.go - user auth middleware tests - backend/tests/integration/balance_api_admin_jwt_test.go - API auth integration

Example Test Pattern:

func TestUserAuthMiddleware(t *testing.T) {
    cfg := config.LoadTestConfig()
    jwtService := jwt_service.NewJWTService(cfg)

    userToken, _ := jwtService.CreateUserToken(123, "user@saga-test.com", "0x742d35...")

    testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := r.Context().Value("user_id").(int64)
        email := r.Context().Value("email").(string)
        walletAddr := r.Context().Value("wallet_address").(string)

        assert.Equal(t, int64(123), userID)
        assert.Equal(t, "user@saga-test.com", email)
        assert.Equal(t, "0x742d35...", walletAddr)

        w.WriteHeader(http.StatusOK)
    })

    handler := middleware.RequireAuth(cfg)(testHandler)

    req := httptest.NewRequest("GET", "/api/user/balance", nil)
    req.Header.Set("Authorization", "Bearer "+userToken)

    rr := httptest.NewRecorder()
    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusOK, rr.Code)
}

2E Testing with Playwright

Admin E2E Authentication

File: frontend/e2e/tests/admin/auth.spec.ts

import { test, expect } from '@playwright/test';
import { generateAdminJWT } from '../../utils/admin-jwt-helper';

test.describe('Admin JWT Authentication E2E', () => {
  test('Admin can access protected endpoints with valid JWT', async ({ page }) => {
    // Generate admin token
    const adminToken = await generateAdminJWT('admin@saga-test.com');

    // Set Authorization header for all requests
    await page.setExtraHTTPHeaders({
      'Authorization': `Bearer ${adminToken}`
    });

    // Navigate to admin page
    await page.goto('http://admin.saga.local:8080/');

    // Verify admin access
    await expect(page.locator('text=Admin Dashboard')).toBeVisible();

    // Test protected API call
    const response = await page.request.get('http://localhost:8080/api/admin/users', {
      headers: {
        'Authorization': `Bearer ${adminToken}`
      }
    });

    expect(response.status()).toBe(200);

    const data = await response.json();
    expect(Array.isArray(data)).toBe(true);
  });

  test('Admin cannot access with invalid JWT', async ({ page }) => {
    const invalidToken = 'invalid.jwt.token';

    await page.setExtraHTTPHeaders({
      'Authorization': `Bearer ${invalidToken}`
    });

    const response = await page.request.get('http://localhost:8080/api/admin/users', {
      headers: {
        'Authorization': `Bearer ${invalidToken}`
      },
      failOnStatusCode: false
    });

    expect(response.status()).toBe(401);
  });

  test('Admin cannot access with expired JWT', async ({ page }) => {
    // Generate token with 1-second expiration
    const expiredToken = await generateAdminJWT('admin@saga-test.com', 1);

    // Wait for expiration
    await new Promise(resolve => setTimeout(resolve, 2000));

    const response = await page.request.get('http://localhost:8080/api/admin/users', {
      headers: {
        'Authorization': `Bearer ${expiredToken}`
      },
      failOnStatusCode: false
    });

    expect(response.status()).toBe(401);
    const error = await response.json();
    expect(error.error).toContain('expired');
  });
});

User E2E Authentication

File: frontend/e2e/tests/user/auth.spec.ts

test.describe('User JWT Authentication E2E', () => {
  test('User can access user endpoints with valid JWT', async ({ page }) => {
    // Create test user and get token
    const { token, userId } = await createTestUserAndGetToken('user@saga-test.com');

    await page.setExtraHTTPHeaders({
      'Authorization': `Bearer ${token}`
    });

    // Navigate to user page
    await page.goto('http://app.saga.local:8080/');

    // Verify user access
    await expect(page.locator('text=My Balance')).toBeVisible();

    // Test protected API
    const response = await page.request.get('http://localhost:8080/api/user/balance', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    expect(response.status()).toBe(200);

    const data = await response.json();
    expect(data.user_id).toBe(userId);
  });

  test('User cannot access admin endpoints', async ({ page }) => {
    const { token } = await createTestUserAndGetToken('user@saga-test.com');

    const response = await page.request.get('http://localhost:8080/api/admin/users', {
      headers: {
        'Authorization': `Bearer ${token}`
      },
      failOnStatusCode: false
    });

    expect(response.status()).toBe(403);
  });
});

JWT Helper Utilities for E2E

File: frontend/e2e/utils/admin-jwt-helper.ts

import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export async function generateAdminJWT(email: string, expirationSeconds?: number): Promise<string> {
  try {
    // Use Makefile command to generate token
    const { stdout } = await execAsync(`make jwt-admin-token EMAIL=${email}`);
    const token = stdout.trim();

    if (!token || token.includes('error')) {
      throw new Error(`Failed to generate admin JWT: ${stdout}`);
    }

    return token;
  } catch (error) {
    console.error('Error generating admin JWT:', error);
    throw error;
  }
}

export async function validateJWT(token: string): Promise<boolean> {
  try {
    const { stdout } = await execAsync(`make jwt-validate TOKEN=${token}`);
    return stdout.includes('Token Valid');
  } catch (error) {
    return false;
  }
}

File: frontend/e2e/utils/real-auth-helpers.ts

export async function createTestUserAndGetToken(email: string): Promise<{ token: string; userId: number }> {
  // Create user in database
  const user = await createTestUser(email);

  // Generate user JWT using backend helper
  const { stdout } = await execAsync(`make jwt-user EMAIL=${email}`);
  const token = extractTokenFromOutput(stdout);

  return {
    token,
    userId: user.id
  };
}

function extractTokenFromOutput(output: string): string {
  // Extract JWT token from make command output
  const lines = output.split('\n');
  const tokenLine = lines.find(line => line.startsWith('eyJ'));
  return tokenLine?.trim() || '';
}

Running JWT Tests

Unit Tests

# Run all JWT-related unit tests
go test ./backend/auth/service/... -v
go test ./backend/shared/http/... -v
go test ./backend/shared/middleware/... -v

# Run specific test
go test ./backend/auth/service -run TestGenerateAdminToken -v

# Run with coverage
go test ./backend/auth/service/... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out

Integration Tests

# Run integration tests with JWT
make core

# Run specific integration test
cd backend && go test ./tests/integration -run TestAdminAPIAuth -v

2E Tests

# Run admin auth E2E tests
make e2e-admin-quick

# Run user auth E2E tests
make e2e-smoke

# Run specific E2E test
make e2e-single FILE=tests/admin/auth.spec.ts

ll Test Suite

# Run all tests (unit + integration + E2E)
make test-all

# Run foundation test suite (smoke + unit + core + e2e-smoke)
make test

Troubleshooting JWT Tests

Common Issues

Issue: "signature is invalid"

# Check JWT_SECRET consistency
echo $JWT_SECRET
docker exec saga printenv JWT_SECRET

# Regenerate tokens with correct secret
export JWT_SECRET="test-jwt-secret-for-smoke-tests-minimum-32-chars-required"
make jwt-admin EMAIL=admin@saga-test.com

Issue: "token contains an invalid number of segments"

# Verify token format (should have 3 parts: header.payload.signature)
echo "<token>" | awk -F. '{print NF-1}'  # Should output: 2

# xtract and decode parts
echo "<header>" | base64 -d
echo "<payload>" | base64 -d

Issue: E2E test fails with 401

# Debug: Check token generation
TOKEN=$(make jwt-admin-token EMAIL=admin@saga-test.com)
echo "Token: $TOKEN"

# Debug: Validate token
make jwt-validate TOKEN=$TOKEN

# Debug: Test API directly
curl -v -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/admin/ping

Issue: Admin test fails with 403 (Forbidden)

# Check admin configuration
grep -A 5 "ADMIN_WALLETS\|admins:" config/auth.yaml

# Verify email is in admin list
rg "admin@saga-test.com" config/

Test Coverage Goals

JWT System Coverage Targets:

  • Unit Tests: >90% coverage
  • jwt_mvp.go: 95% (critical path)
  • jwt_validator.go: 90%
  • token_manager.go: 85%

  • Integration Tests: >80% coverage

  • Admin auth middleware: 90%
  • User auth middleware: 90%
  • Protected endpoints: 85%

  • E2E Tests: >70% coverage

  • Admin authentication flows: 80%
  • User authentication flows: 80%
  • Token expiration scenarios: 70%

Running Coverage Reports:

# Generate JWT service coverage
cd backend && go test ./auth/service/... -coverprofile=jwt-coverage.out
go tool cover -html=jwt-coverage.out -o jwt-coverage.html

# Generate middleware coverage
go test ./shared/middleware/... -coverprofile=middleware-coverage.out
go tool cover -html=middleware-coverage.out



📋 Метаданные

Версия: 2.4.82

Обновлено: 2025-10-21

Статус: Published