150 lines
5.3 KiB
Go
150 lines
5.3 KiB
Go
package services
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestJWTService(t *testing.T) {
|
|
secret := "test-secret-key-for-unit-tests-very-secure"
|
|
issuer := "veza-api"
|
|
audience := "veza-app"
|
|
jwtService, err := NewJWTService("", "", secret, issuer, audience)
|
|
assert.NoError(t, err)
|
|
|
|
// Mock User
|
|
// GO-004: Utiliser UUID au lieu de int
|
|
userID := uuid.New()
|
|
user := &models.User{
|
|
ID: userID,
|
|
Email: "test@example.com",
|
|
Username: "testuser",
|
|
Role: "user",
|
|
TokenVersion: 5,
|
|
}
|
|
|
|
t.Run("GenerateAccessToken", func(t *testing.T) {
|
|
token, err := jwtService.GenerateAccessToken(user)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, token)
|
|
|
|
// Validate immediately
|
|
claims, err := jwtService.ValidateToken(token)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, user.ID, claims.UserID)
|
|
assert.Equal(t, user.Email, claims.Email)
|
|
assert.Equal(t, user.Role, claims.Role)
|
|
})
|
|
|
|
t.Run("GenerateRefreshToken", func(t *testing.T) {
|
|
token, err := jwtService.GenerateRefreshToken(user)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, token)
|
|
|
|
// Validate
|
|
claims, err := jwtService.ValidateToken(token)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, user.ID, claims.UserID)
|
|
// Refresh token doesn't have email in current implementation
|
|
assert.Empty(t, claims.Email)
|
|
})
|
|
|
|
t.Run("VerifyTokenVersion", func(t *testing.T) {
|
|
// Generate token with user.TokenVersion = 5
|
|
token, _ := jwtService.GenerateAccessToken(user)
|
|
claims, _ := jwtService.ValidateToken(token)
|
|
|
|
// Case 1: Same version -> OK
|
|
err := jwtService.VerifyTokenVersion(claims, 5)
|
|
assert.NoError(t, err)
|
|
|
|
// Case 2: DB version is higher -> Error
|
|
err = jwtService.VerifyTokenVersion(claims, 6)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "token version mismatch")
|
|
|
|
// Case 3: DB version is lower -> OK (assuming implementation allows older tokens if logic permits, but usually equality is checked.
|
|
// Let's check implementation logic: return claims.TokenVersion != currentVersion
|
|
err = jwtService.VerifyTokenVersion(claims, 4)
|
|
assert.Error(t, err) // Expect error because version must match
|
|
})
|
|
|
|
t.Run("ExpiredToken", func(t *testing.T) {
|
|
invalidToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.invalid_signature"
|
|
_, err := jwtService.ValidateToken(invalidToken)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("Security_StrictValidation", func(t *testing.T) {
|
|
// Test Invalid Issuer
|
|
invalidIssuerClaims := models.CustomClaims{
|
|
UserID: userID,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
Issuer: "evil.com", // Wrong issuer
|
|
Audience: jwt.ClaimStrings{audience},
|
|
},
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, invalidIssuerClaims)
|
|
tokenString, _ := token.SignedString([]byte(secret))
|
|
_, err := jwtService.ValidateToken(tokenString)
|
|
assert.Error(t, err)
|
|
// The error might be "issuer name 'evil.com' is invalid" or "token has invalid issuer"
|
|
assert.True(t,
|
|
strings.Contains(err.Error(), "issuer name") ||
|
|
strings.Contains(err.Error(), "invalid issuer"),
|
|
"Error should mention issuer validation issue, got: %s", err.Error())
|
|
|
|
// Test Invalid Audience
|
|
invalidAudClaims := models.CustomClaims{
|
|
UserID: userID,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
Issuer: issuer,
|
|
Audience: jwt.ClaimStrings{"wrong-app"}, // Wrong audience
|
|
},
|
|
}
|
|
token = jwt.NewWithClaims(jwt.SigningMethodHS256, invalidAudClaims)
|
|
tokenString, _ = token.SignedString([]byte(secret))
|
|
_, err = jwtService.ValidateToken(tokenString)
|
|
assert.Error(t, err)
|
|
// The error might be "token contains an invalid number of audience claims" or "token has invalid audience"
|
|
assert.True(t,
|
|
strings.Contains(err.Error(), "invalid number of audience claims") ||
|
|
strings.Contains(err.Error(), "invalid audience"),
|
|
"Error should mention audience validation issue, got: %s", err.Error())
|
|
|
|
// Test Invalid Algorithm (HS512 instead of HS256)
|
|
// Note: We need to use the SAME secret but different alg to verify alg check works even if signature verifies (if library allowed it, but strict parsing should fail alg)
|
|
validClaims := models.CustomClaims{
|
|
UserID: userID,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
Issuer: issuer,
|
|
Audience: jwt.ClaimStrings{audience},
|
|
},
|
|
}
|
|
token = jwt.NewWithClaims(jwt.SigningMethodHS512, validClaims)
|
|
tokenString, _ = token.SignedString([]byte(secret))
|
|
_, err = jwtService.ValidateToken(tokenString)
|
|
assert.Error(t, err)
|
|
// The error might be "unexpected signing method", "invalid signing algorithm", "signature is invalid",
|
|
// or "invalid audience" depending on implementation details and validation order
|
|
// Our code returns "invalid signing algorithm: HS512, expected HS256" or "token has invalid audience"
|
|
assert.True(t,
|
|
strings.Contains(err.Error(), "unexpected signing method") ||
|
|
strings.Contains(err.Error(), "invalid signing algorithm") ||
|
|
strings.Contains(err.Error(), "signature is invalid") ||
|
|
strings.Contains(err.Error(), "invalid audience") ||
|
|
strings.Contains(err.Error(), "invalid number of audience"),
|
|
"Error should mention validation issue, got: %s", err.Error())
|
|
})
|
|
}
|