package auth import ( "encoding/base64" "strings" "testing" "time" "github.com/golang-jwt/jwt/v5" ) const testSecret = "test-secret-at-least-32-characters-long" func TestGenerateAndParseToken(t *testing.T) { token, err := GenerateToken(testSecret, "user-1", "worker") if err != nil { t.Fatalf("generate: %v", err) } claims, err := ParseToken(testSecret, token) if err != nil { t.Fatalf("parse: %v", err) } if claims.UserID != "user-1" || claims.Role != "worker" { t.Errorf("claims = %+v, want user_id=user-1 role=worker", claims) } } func TestParseTokenRejects(t *testing.T) { cases := []struct { name string token string }{ { name: "подписан другим секретом", token: sign(t, "another-secret", jwt.SigningMethodHS256, time.Hour), }, { name: "другой алгоритм при том же секрете (HS512)", token: sign(t, testSecret, jwt.SigningMethodHS512, time.Hour), }, { name: "просроченный", token: sign(t, testSecret, jwt.SigningMethodHS256, -time.Hour), }, { name: "alg=none, подпись не подставлена", token: forgeNone(t), }, { name: "мусор вместо токена", token: "not-a-token", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if _, err := ParseToken(testSecret, tc.token); err == nil { t.Error("token was accepted, want rejection") } }) } } func TestEmptySecretFailsFast(t *testing.T) { if _, err := GenerateToken("", "user-1", "client"); err == nil { t.Error("GenerateToken with empty secret: want error, got token") } if _, err := ParseToken("", "whatever"); err == nil { t.Error("ParseToken with empty secret: want error, got claims") } } // sign собирает токен вручную, чтобы протестировать сценарии, // которые GenerateToken сам не порождает. func sign(t *testing.T, secret string, method jwt.SigningMethod, ttl time.Duration) string { t.Helper() claims := Claims{ UserID: "user-1", Role: "admin", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token, err := jwt.NewWithClaims(method, claims).SignedString([]byte(secret)) if err != nil { t.Fatalf("sign: %v", err) } return token } // forgeNone подделывает токен с alg=none: раньше такой заголовок // проходил, потому что ключевая функция не сверяла метод подписи. func forgeNone(t *testing.T) string { t.Helper() header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`)) payload := base64.RawURLEncoding.EncodeToString( []byte(`{"user_id":"hacker","role":"admin","exp":4102444800}`), ) return strings.Join([]string{header, payload, ""}, ".") }