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()) }) }