Skip to content

Тестирование и внедрение часов

Через интерфейс ClockProvider можно внедрить пользовательские часы для точного контроля времени в тестах.

Интерфейс ClockProvider

go
type ClockProvider interface {
    Now() time.Time
}

Библиотека предоставляет две реализации:

ТипОписание
SystemClockПо умолчанию, использует системное время
FixedClockФиксированное время для тестирования

FixedClock

FixedClock всегда возвращает время, указанное при конструировании:

go
fixedTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.Clock = jwt.FixedClock{T: fixedTime}

Тестирование истечения токена

go
func TestTokenExpiry(t *testing.T) {
    // Установка фиксированного времени
    now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    cfg.AccessTokenTTL = 15 * time.Minute
    cfg.Clock = jwt.FixedClock{T: now}

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    // Выпуск токена в момент now
    claims := &jwt.Claims{UserID: "user123"}
    token, err := processor.Create(claims)
    require.NoError(t, err)

    // Проверка в текущий момент → успех
    _, valid, err := processor.Validate(token)
    require.NoError(t, err)
    assert.True(t, valid)

    // Имитация прошествия времени после истечения → используем новый Processor
    expiredCfg := cfg
    expiredCfg.Clock = jwt.FixedClock{T: now.Add(16 * time.Minute)}
    expiredProcessor, err := jwt.New(expiredCfg)
    require.NoError(t, err)
    defer expiredProcessor.Close()

    _, _, err = expiredProcessor.Validate(token)
    assert.True(t, errors.Is(err, jwt.ErrTokenExpired))
}

Тестирование допуска часов (ClockSkew)

ClockSkew предоставляет окно допустимости для проверки exp (истечение) и nbf (не ранее). Задав допуск, можно проверить, принимается ли токен в течение короткого времени после строгого момента истечения:

go
func TestClockSkew(t *testing.T) {
    now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

    // Выпуск токена: exp = now + 1h
    issueCfg := jwt.DefaultConfig()
    issueCfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    issueCfg.AccessTokenTTL = time.Hour
    issueCfg.Clock = jwt.FixedClock{T: now}

    issueProc, err := jwt.New(issueCfg)
    require.NoError(t, err)
    defer issueProc.Close()

    token, err := issueProc.Create(&jwt.Claims{UserID: "user123"})
    require.NoError(t, err)

    const skew = 30 * time.Second

    // exp + 10s всё ещё в 30s окне допуска → действителен
    withinCfg := jwt.DefaultConfig()
    withinCfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    withinCfg.ClockSkew = skew
    withinCfg.Clock = jwt.FixedClock{T: now.Add(time.Hour + 10*time.Second)}
    withinProc, err := jwt.New(withinCfg)
    require.NoError(t, err)
    defer withinProc.Close()

    _, valid, err := withinProc.Validate(token)
    require.NoError(t, err)
    assert.True(t, valid)

    // exp + 40s выходит за 30s окно допуска → истёк
    beyondCfg := jwt.DefaultConfig()
    beyondCfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    beyondCfg.ClockSkew = skew
    beyondCfg.Clock = jwt.FixedClock{T: now.Add(time.Hour + 40*time.Second)}
    beyondProc, err := jwt.New(beyondCfg)
    require.NoError(t, err)
    defer beyondProc.Close()

    _, _, err = beyondProc.Validate(token)
    assert.True(t, errors.Is(err, jwt.ErrTokenExpired))
}

FixedClock и ограничение скорости

При включённом ограничении скорости часы встроенного RateLimiter наследуются от Config.Clock — при использовании FixedClock ограничитель также использует фиксированное время и не пополняет токены с течением реального времени. Это делает тесты ограничения скорости полностью предсказуемыми. См. Тестирование ограничения скорости.

Тестирование потока обновления

go
func TestRefreshFlow(t *testing.T) {
    now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    cfg.Clock = jwt.FixedClock{T: now}

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    claims := &jwt.Claims{UserID: "user123"}
    refreshToken, err := processor.CreateRefresh(claims)
    require.NoError(t, err)

    // Получение нового токена доступа через токен обновления
    newToken, err := processor.Refresh(refreshToken)
    require.NoError(t, err)
    assert.NotEmpty(t, newToken)
}

Тестирование пользовательских Claims

go
func TestCustomClaims(t *testing.T) {
    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    claims := &MyClaims{
        UserID: "user123",
        Email:  "[email protected]",
    }

    token, err := processor.Create(claims)
    require.NoError(t, err)

    result := &MyClaims{}
    parsed, valid, err := processor.ValidateInto(token, result)
    require.NoError(t, err)
    assert.True(t, valid)

    myResult := parsed.(*MyClaims)
    assert.Equal(t, "user123", myResult.UserID)
    assert.Equal(t, "[email protected]", myResult.Email)
}

Тестирование валидации ввода

Поля Claims при Create проходят многоуровневую валидацию. Тесты могут проверять, вызывают ли сверхдлинные строки, инъекционные шаблоны и управляющие символы ValidationError, и извлекать информацию уровня поля через errors.As:

go
func TestInputValidation(t *testing.T) {
    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    var ve *jwt.ValidationError

    // Сверхдлинная строка вызывает ограничение длины (максимум 256 символов)
    _, err = processor.Create(&jwt.Claims{
        UserID: strings.Repeat("a", 300),
    })
    require.ErrorAs(t, err, &ve)
    assert.Equal(t, "UserID", ve.Field)
    assert.Contains(t, ve.Message, "maximum length")

    // Обнаружение XSS-инъекции
    _, err = processor.Create(&jwt.Claims{
        UserID: "<script>alert(1)</script>",
    })
    require.ErrorAs(t, err, &ve)
    assert.Equal(t, "UserID", ve.Field)
    assert.Equal(t, "suspicious pattern detected", ve.Message)

    // Фильтрация управляющих символов (null-байт отклоняется)
    _, err = processor.Create(&jwt.Claims{
        UserID: "user\x00inject",
    })
    require.ErrorAs(t, err, &ve)
    assert.Equal(t, "UserID", ve.Field)
    assert.Equal(t, "invalid control character", ve.Message)
}

Уровни обёртки ValidationError

В пути Create ValidationError оборачивается в ErrInvalidClaims. Использование errors.As(err, &ve) пробивается сквозь обёртку и извлекает ValidationError, позволяя читать Field и Message для утверждений. Полное описание правил валидации см. в Подробная конфигурация → Проверка ввода и усиление безопасности.

Тестирование обработки ошибок

go
func TestRevokeToken(t *testing.T) {
    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    claims := &jwt.Claims{UserID: "user123"}
    token, err := processor.Create(claims)
    require.NoError(t, err)

    // Отзыв токена
    err = processor.Revoke(token)
    require.NoError(t, err)

    // Проверка должна завершиться ошибкой
    _, _, err = processor.Validate(token)
    assert.True(t, errors.Is(err, jwt.ErrTokenRevoked))
}

Тестирование ограничения скорости

После включения ограничения скорости Create сверх квоты возвращает ErrRateLimitExceeded. В сочетании с FixedClock можно точно контролировать отсутствие пополнения корзины токенов, делая тест полностью предсказуемым:

go
func TestRateLimit(t *testing.T) {
    now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    cfg.EnableRateLimit = true
    cfg.RateLimitRate = 3               // 3 раза за окно
    cfg.RateLimitWindow = time.Minute
    cfg.Clock = jwt.FixedClock{T: now}  // Фиксированное время → корзина токенов не пополняется

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    // Один и тот же UserID делит квоту скорости (при пустом Subject откат к UserID)
    claims := &jwt.Claims{UserID: "user123"}

    // Первые 3 создания успешны
    for i := 0; i < 3; i++ {
        _, err := processor.Create(claims)
        require.NoError(t, err, "Создание %d должно быть успешным", i+1)
    }

    // 4-е превышает квоту
    _, err = processor.Create(claims)
    assert.True(t, errors.Is(err, jwt.ErrRateLimitExceeded))
}

Ключ ограничения скорости

Ограничение скорости рассчитывается по утверждению Subject; при пустом Subject откат к UserID. В тестах использование одного и того же UserID гарантирует, что все запросы делят квоту. Подробнее см. Ограничение скорости.

Тестирование потокобезопасности

Все методы Processor goroutine-безопасны. Используйте sync.WaitGroup для параллельного выполнения Create/Validate, проверяя отсутствие panic и data race:

go
func TestConcurrentSafety(t *testing.T) {
    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"

    processor, err := jwt.New(cfg)
    require.NoError(t, err)
    defer processor.Close()

    const goroutines = 50
    const opsPerGoroutine = 20

    var wg sync.WaitGroup
    var success atomic.Int64
    wg.Add(goroutines)

    for i := 0; i < goroutines; i++ {
        go func(id int) {
            defer wg.Done()
            for j := 0; j < opsPerGoroutine; j++ {
                claims := &jwt.Claims{
                    UserID: fmt.Sprintf("user-%d-%d", id, j),
                }
                token, err := processor.Create(claims)
                if err != nil {
                    continue
                }
                if _, valid, err := processor.Validate(token); err == nil && valid {
                    success.Add(1)
                }
            }
        }(i)
    }
    wg.Wait()

    assert.Equal(t, int64(goroutines*opsPerGoroutine), success.Load(),
        "Параллельные создание и проверка должны быть полностью успешными")
}

Обнаружение гонок

Запускайте go test -race ./... для включения детектора гонок Go, который может выявить скрытые data race в параллельных тестах. Это стандартный способ проверки потокобезопасности Processor; тестовые наборы production-кода всегда должны проходить под -race.

Лучшие практики

Рекомендации по табличным тестам

СценарийРекомендуемый подходКлючевые утверждения
Истечение токенаFixedClock фиксирует время выпуска, новый Processor имитирует истечениеerrors.Is(err, ErrTokenExpired)
Допуск часовЗадать ClockSkew, проверить границу окнаВ пределах exp + skew действителен / вне — истёк
Поток обновленияСразу Refresh после создания refresh-токенаВозвращённый токен непустой
Пользовательские ClaimsValidateInto десериализует в целевой типЗначения полей совпадают
Валидация вводаСверхдлинные строки / инъекции / управляющие символыerrors.As извлекает ValidationError
Ограничение скоростиМалое окно + низкая скорость + фиксированные часыПревышение возвращает ErrRateLimitExceeded
Потокобезопасностьgoroutine + WaitGroup для параллельных операцийБез panic, без data race
Отзыв токенаRevoke затем Validateerrors.Is(err, ErrTokenRevoked)

Основные принципы

  • Используйте FixedClock для обеспечения воспроизводимости тестов — без зависимости от системного времени
  • Создавайте независимый Processor для каждого теста во избежание утечки состояния
  • Используйте t.Cleanup() или defer для гарантии вызова Close()
  • При проверке ошибок используйте errors.Is() / errors.As() вместо сопоставления строк
  • Параллельные тесты всегда запускайте с go test -race

Дальнейшие шаги