mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
c474bbafd6
* Cleanup excessive comments. * Remove leftovers hanging around from previous refactor * Improve test coverage
621 lines
16 KiB
Go
621 lines
16 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/traefikoidc/internal/testutil"
|
|
"github.com/stretchr/testify/suite"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// ClockSkewEdgeCasesSuite tests clock skew tolerance scenarios
|
|
type ClockSkewEdgeCasesSuite struct {
|
|
suite.Suite
|
|
|
|
fixture *testutil.TokenFixture
|
|
tOidc *TraefikOidc
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) SetupSuite() {
|
|
var err error
|
|
s.fixture, err = testutil.NewTokenFixture()
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) SetupTest() {
|
|
// Create JWK for the test key
|
|
jwk := JWK{
|
|
Kty: "RSA",
|
|
Kid: s.fixture.KeyID,
|
|
Alg: "RS256",
|
|
N: base64.RawURLEncoding.EncodeToString(s.fixture.RSAPublicKey.N.Bytes()),
|
|
E: base64.RawURLEncoding.EncodeToString(bigIntToBytes(big.NewInt(int64(s.fixture.RSAPublicKey.E)))),
|
|
}
|
|
|
|
jwkCache := &MockJWKCache{
|
|
JWKS: &JWKSet{Keys: []JWK{jwk}},
|
|
Err: nil,
|
|
}
|
|
|
|
tokenBlacklist := NewCache()
|
|
tokenCacheInternal := NewCache()
|
|
tokenCache := &TokenCache{}
|
|
if tokenCache.cache == nil {
|
|
if wrapper, ok := tokenCacheInternal.(*CacheInterfaceWrapper); ok {
|
|
tokenCache.cache = wrapper.cache
|
|
}
|
|
}
|
|
|
|
logger := NewLogger("error") // Reduce noise
|
|
|
|
s.tOidc = &TraefikOidc{
|
|
issuerURL: s.fixture.Issuer,
|
|
clientID: s.fixture.Audience,
|
|
audience: s.fixture.Audience,
|
|
clientSecret: "test-client-secret",
|
|
roleClaimName: "roles",
|
|
groupClaimName: "groups",
|
|
userIdentifierClaim: "email",
|
|
jwkCache: jwkCache,
|
|
jwksURL: "https://test-jwks-url.com",
|
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
|
tokenBlacklist: tokenBlacklist,
|
|
tokenCache: tokenCache,
|
|
logger: logger,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
extractClaimsFunc: extractClaims,
|
|
initComplete: make(chan struct{}),
|
|
goroutineWG: &sync.WaitGroup{},
|
|
ctx: context.Background(),
|
|
}
|
|
close(s.tOidc.initComplete)
|
|
s.tOidc.tokenVerifier = s.tOidc
|
|
s.tOidc.jwtVerifier = s.tOidc
|
|
|
|
s.T().Cleanup(func() {
|
|
if s.tOidc.tokenBlacklist != nil {
|
|
s.tOidc.tokenBlacklist.Close()
|
|
}
|
|
if s.tOidc.tokenCache != nil && s.tOidc.tokenCache.cache != nil {
|
|
s.tOidc.tokenCache.cache.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) TestExactlyAtExpiry() {
|
|
token, err := s.fixture.TokenWithSkew(0)
|
|
s.Require().NoError(err)
|
|
|
|
// Token at exact expiry - behavior is implementation-defined
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.T().Logf("Exact expiry result: %v", err)
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) TestOneSecondBeforeExpiry() {
|
|
token, err := s.fixture.TokenWithSkew(1 * time.Second)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Token should be valid 1 second before expiry")
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) TestOneSecondAfterExpiry() {
|
|
token, err := s.fixture.TokenWithSkew(-1 * time.Second)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
// With default 2-minute clock skew tolerance, 1 second past expiry should still be valid
|
|
s.NoError(err, "Token 1 second past expiry should be valid within clock skew tolerance")
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) TestWithinSkewTolerance() {
|
|
// Most implementations allow 5-minute clock skew
|
|
token, err := s.fixture.TokenWithSkew(-4 * time.Minute)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
// May pass or fail depending on implementation
|
|
s.T().Logf("4-minute expired token result: %v", err)
|
|
}
|
|
|
|
func (s *ClockSkewEdgeCasesSuite) TestBeyondSkewTolerance() {
|
|
token, err := s.fixture.TokenWithSkew(-10 * time.Minute)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.Error(err, "Token should be invalid 10 minutes after expiry")
|
|
}
|
|
|
|
func TestClockSkewEdgeCasesSuite(t *testing.T) {
|
|
suite.Run(t, new(ClockSkewEdgeCasesSuite))
|
|
}
|
|
|
|
// UnicodeClaimsSuite tests Unicode handling in JWT claims
|
|
type UnicodeClaimsSuite struct {
|
|
suite.Suite
|
|
|
|
fixture *testutil.TokenFixture
|
|
tOidc *TraefikOidc
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) SetupSuite() {
|
|
var err error
|
|
s.fixture, err = testutil.NewTokenFixture()
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) SetupTest() {
|
|
jwk := JWK{
|
|
Kty: "RSA",
|
|
Kid: s.fixture.KeyID,
|
|
Alg: "RS256",
|
|
N: base64.RawURLEncoding.EncodeToString(s.fixture.RSAPublicKey.N.Bytes()),
|
|
E: base64.RawURLEncoding.EncodeToString(bigIntToBytes(big.NewInt(int64(s.fixture.RSAPublicKey.E)))),
|
|
}
|
|
|
|
jwkCache := &MockJWKCache{
|
|
JWKS: &JWKSet{Keys: []JWK{jwk}},
|
|
Err: nil,
|
|
}
|
|
|
|
tokenBlacklist := NewCache()
|
|
tokenCacheInternal := NewCache()
|
|
tokenCache := &TokenCache{}
|
|
if tokenCache.cache == nil {
|
|
if wrapper, ok := tokenCacheInternal.(*CacheInterfaceWrapper); ok {
|
|
tokenCache.cache = wrapper.cache
|
|
}
|
|
}
|
|
|
|
logger := NewLogger("error")
|
|
|
|
s.tOidc = &TraefikOidc{
|
|
issuerURL: s.fixture.Issuer,
|
|
clientID: s.fixture.Audience,
|
|
audience: s.fixture.Audience,
|
|
clientSecret: "test-client-secret",
|
|
roleClaimName: "roles",
|
|
groupClaimName: "groups",
|
|
userIdentifierClaim: "email",
|
|
jwkCache: jwkCache,
|
|
jwksURL: "https://test-jwks-url.com",
|
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
|
tokenBlacklist: tokenBlacklist,
|
|
tokenCache: tokenCache,
|
|
logger: logger,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
extractClaimsFunc: extractClaims,
|
|
initComplete: make(chan struct{}),
|
|
goroutineWG: &sync.WaitGroup{},
|
|
ctx: context.Background(),
|
|
}
|
|
close(s.tOidc.initComplete)
|
|
s.tOidc.tokenVerifier = s.tOidc
|
|
s.tOidc.jwtVerifier = s.tOidc
|
|
|
|
s.T().Cleanup(func() {
|
|
if s.tOidc.tokenBlacklist != nil {
|
|
s.tOidc.tokenBlacklist.Close()
|
|
}
|
|
if s.tOidc.tokenCache != nil && s.tOidc.tokenCache.cache != nil {
|
|
s.tOidc.tokenCache.cache.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) TestUnicodeEmail() {
|
|
token, err := s.fixture.TokenWithEmail("用户@example.com")
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Unicode email should be handled correctly")
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) TestUnicodeName() {
|
|
token, err := s.fixture.TokenWithCustomClaims(map[string]interface{}{
|
|
"name": "田中太郎",
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Unicode name should be handled correctly")
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) TestEmojiInClaims() {
|
|
token, err := s.fixture.TokenWithCustomClaims(map[string]interface{}{
|
|
"name": "Test User 😀",
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Emoji in claims should be handled correctly")
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) TestRTLText() {
|
|
token, err := s.fixture.TokenWithCustomClaims(map[string]interface{}{
|
|
"name": "مستخدم اختبار",
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "RTL text should be handled correctly")
|
|
}
|
|
|
|
func (s *UnicodeClaimsSuite) TestMixedScripts() {
|
|
token, err := s.fixture.TokenWithCustomClaims(map[string]interface{}{
|
|
"name": "Test 测试 テスト",
|
|
"roles": []string{"admin", "管理者", "管理员"},
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Mixed scripts should be handled correctly")
|
|
}
|
|
|
|
func TestUnicodeClaimsSuite(t *testing.T) {
|
|
suite.Run(t, new(UnicodeClaimsSuite))
|
|
}
|
|
|
|
// LargeClaimsSuite tests large claim values
|
|
type LargeClaimsSuite struct {
|
|
suite.Suite
|
|
|
|
fixture *testutil.TokenFixture
|
|
tOidc *TraefikOidc
|
|
}
|
|
|
|
func (s *LargeClaimsSuite) SetupSuite() {
|
|
var err error
|
|
s.fixture, err = testutil.NewTokenFixture()
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
func (s *LargeClaimsSuite) SetupTest() {
|
|
jwk := JWK{
|
|
Kty: "RSA",
|
|
Kid: s.fixture.KeyID,
|
|
Alg: "RS256",
|
|
N: base64.RawURLEncoding.EncodeToString(s.fixture.RSAPublicKey.N.Bytes()),
|
|
E: base64.RawURLEncoding.EncodeToString(bigIntToBytes(big.NewInt(int64(s.fixture.RSAPublicKey.E)))),
|
|
}
|
|
|
|
jwkCache := &MockJWKCache{
|
|
JWKS: &JWKSet{Keys: []JWK{jwk}},
|
|
Err: nil,
|
|
}
|
|
|
|
tokenBlacklist := NewCache()
|
|
tokenCacheInternal := NewCache()
|
|
tokenCache := &TokenCache{}
|
|
if tokenCache.cache == nil {
|
|
if wrapper, ok := tokenCacheInternal.(*CacheInterfaceWrapper); ok {
|
|
tokenCache.cache = wrapper.cache
|
|
}
|
|
}
|
|
|
|
logger := NewLogger("error")
|
|
|
|
s.tOidc = &TraefikOidc{
|
|
issuerURL: s.fixture.Issuer,
|
|
clientID: s.fixture.Audience,
|
|
audience: s.fixture.Audience,
|
|
clientSecret: "test-client-secret",
|
|
roleClaimName: "roles",
|
|
groupClaimName: "groups",
|
|
userIdentifierClaim: "email",
|
|
jwkCache: jwkCache,
|
|
jwksURL: "https://test-jwks-url.com",
|
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
|
tokenBlacklist: tokenBlacklist,
|
|
tokenCache: tokenCache,
|
|
logger: logger,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
extractClaimsFunc: extractClaims,
|
|
initComplete: make(chan struct{}),
|
|
goroutineWG: &sync.WaitGroup{},
|
|
ctx: context.Background(),
|
|
}
|
|
close(s.tOidc.initComplete)
|
|
s.tOidc.tokenVerifier = s.tOidc
|
|
s.tOidc.jwtVerifier = s.tOidc
|
|
|
|
s.T().Cleanup(func() {
|
|
if s.tOidc.tokenBlacklist != nil {
|
|
s.tOidc.tokenBlacklist.Close()
|
|
}
|
|
if s.tOidc.tokenCache != nil && s.tOidc.tokenCache.cache != nil {
|
|
s.tOidc.tokenCache.cache.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *LargeClaimsSuite) TestManyRoles() {
|
|
roles := make([]string, 100)
|
|
for i := 0; i < 100; i++ {
|
|
roles[i] = strings.Repeat("role", 10) + string(rune('A'+i%26))
|
|
}
|
|
|
|
token, err := s.fixture.TokenWithRoles(roles)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Token with 100 roles should be handled")
|
|
}
|
|
|
|
func (s *LargeClaimsSuite) TestManyGroups() {
|
|
groups := make([]string, 50)
|
|
for i := 0; i < 50; i++ {
|
|
groups[i] = strings.Repeat("group", 5) + string(rune('A'+i%26))
|
|
}
|
|
|
|
token, err := s.fixture.TokenWithGroups(groups)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Token with 50 groups should be handled")
|
|
}
|
|
|
|
func (s *LargeClaimsSuite) TestLongEmail() {
|
|
// RFC 5321 allows up to 254 characters
|
|
localPart := strings.Repeat("a", 64)
|
|
domain := strings.Repeat("b", 63) + ".com"
|
|
email := localPart + "@" + domain
|
|
|
|
token, err := s.fixture.TokenWithEmail(email)
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Token with long email should be handled")
|
|
}
|
|
|
|
func (s *LargeClaimsSuite) TestLongSubject() {
|
|
longSub := strings.Repeat("subject", 100)
|
|
|
|
token, err := s.fixture.TokenWithCustomClaims(map[string]interface{}{
|
|
"sub": longSub,
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
err = s.tOidc.VerifyToken(token)
|
|
s.NoError(err, "Token with long subject should be handled")
|
|
}
|
|
|
|
func TestLargeClaimsSuite(t *testing.T) {
|
|
suite.Run(t, new(LargeClaimsSuite))
|
|
}
|
|
|
|
// URLPathEdgeCasesSuite tests URL handling edge cases
|
|
type URLPathEdgeCasesSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func (s *URLPathEdgeCasesSuite) TestVeryLongPath() {
|
|
longPath := "/" + strings.Repeat("segment/", 100)
|
|
req := httptest.NewRequest("GET", longPath, nil)
|
|
|
|
s.NotNil(req)
|
|
s.Contains(req.URL.Path, "segment")
|
|
}
|
|
|
|
func (s *URLPathEdgeCasesSuite) TestSpecialCharactersInPath() {
|
|
paths := []string{
|
|
"/path%20with%20spaces",
|
|
"/path/with/日本語",
|
|
"/path?query=value&another=test",
|
|
"/path#fragment",
|
|
"/path/../traversal",
|
|
"/path/./current",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
s.Run(path, func() {
|
|
req := httptest.NewRequest("GET", path, nil)
|
|
s.NotNil(req)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *URLPathEdgeCasesSuite) TestEmptyPath() {
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
s.Equal("/", req.URL.Path)
|
|
}
|
|
|
|
func (s *URLPathEdgeCasesSuite) TestDoubleSlashes() {
|
|
req := httptest.NewRequest("GET", "//double//slashes//", nil)
|
|
s.NotNil(req)
|
|
}
|
|
|
|
func TestURLPathEdgeCasesSuite(t *testing.T) {
|
|
suite.Run(t, new(URLPathEdgeCasesSuite))
|
|
}
|
|
|
|
// ConcurrencyEdgeCasesSuite tests concurrency scenarios
|
|
type ConcurrencyEdgeCasesSuite struct {
|
|
suite.Suite
|
|
|
|
fixture *testutil.TokenFixture
|
|
tOidc *TraefikOidc
|
|
}
|
|
|
|
func (s *ConcurrencyEdgeCasesSuite) SetupSuite() {
|
|
var err error
|
|
s.fixture, err = testutil.NewTokenFixture()
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
func (s *ConcurrencyEdgeCasesSuite) SetupTest() {
|
|
jwk := JWK{
|
|
Kty: "RSA",
|
|
Kid: s.fixture.KeyID,
|
|
Alg: "RS256",
|
|
N: base64.RawURLEncoding.EncodeToString(s.fixture.RSAPublicKey.N.Bytes()),
|
|
E: base64.RawURLEncoding.EncodeToString(bigIntToBytes(big.NewInt(int64(s.fixture.RSAPublicKey.E)))),
|
|
}
|
|
|
|
jwkCache := &MockJWKCache{
|
|
JWKS: &JWKSet{Keys: []JWK{jwk}},
|
|
Err: nil,
|
|
}
|
|
|
|
tokenBlacklist := NewCache()
|
|
tokenCacheInternal := NewCache()
|
|
tokenCache := &TokenCache{}
|
|
if tokenCache.cache == nil {
|
|
if wrapper, ok := tokenCacheInternal.(*CacheInterfaceWrapper); ok {
|
|
tokenCache.cache = wrapper.cache
|
|
}
|
|
}
|
|
|
|
logger := NewLogger("error")
|
|
|
|
s.tOidc = &TraefikOidc{
|
|
issuerURL: s.fixture.Issuer,
|
|
clientID: s.fixture.Audience,
|
|
audience: s.fixture.Audience,
|
|
clientSecret: "test-client-secret",
|
|
roleClaimName: "roles",
|
|
groupClaimName: "groups",
|
|
userIdentifierClaim: "email",
|
|
jwkCache: jwkCache,
|
|
jwksURL: "https://test-jwks-url.com",
|
|
limiter: rate.NewLimiter(rate.Every(time.Second), 100), // Higher limit for concurrency tests
|
|
tokenBlacklist: tokenBlacklist,
|
|
tokenCache: tokenCache,
|
|
logger: logger,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
extractClaimsFunc: extractClaims,
|
|
initComplete: make(chan struct{}),
|
|
goroutineWG: &sync.WaitGroup{},
|
|
ctx: context.Background(),
|
|
}
|
|
close(s.tOidc.initComplete)
|
|
s.tOidc.tokenVerifier = s.tOidc
|
|
s.tOidc.jwtVerifier = s.tOidc
|
|
|
|
s.T().Cleanup(func() {
|
|
if s.tOidc.tokenBlacklist != nil {
|
|
s.tOidc.tokenBlacklist.Close()
|
|
}
|
|
if s.tOidc.tokenCache != nil && s.tOidc.tokenCache.cache != nil {
|
|
s.tOidc.tokenCache.cache.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *ConcurrencyEdgeCasesSuite) TestConcurrentTokenValidation() {
|
|
token, err := s.fixture.ValidToken(nil)
|
|
s.Require().NoError(err)
|
|
|
|
const goroutines = 50
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, goroutines)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := s.tOidc.VerifyToken(token); err != nil {
|
|
errors <- err
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
var errCount int
|
|
for err := range errors {
|
|
s.T().Logf("Concurrent error: %v", err)
|
|
errCount++
|
|
}
|
|
|
|
s.Equal(0, errCount, "All concurrent validations should succeed")
|
|
}
|
|
|
|
func (s *ConcurrencyEdgeCasesSuite) TestConcurrentDifferentTokens() {
|
|
const goroutines = 20
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, goroutines)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
token, err := s.fixture.TokenWithCustomClaims(map[string]interface{}{
|
|
"custom": idx,
|
|
})
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
if err := s.tOidc.VerifyToken(token); err != nil {
|
|
errors <- err
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
var errCount int
|
|
for err := range errors {
|
|
s.T().Logf("Concurrent different token error: %v", err)
|
|
errCount++
|
|
}
|
|
|
|
s.Equal(0, errCount, "All concurrent different token validations should succeed")
|
|
}
|
|
|
|
func (s *ConcurrencyEdgeCasesSuite) TestConcurrentMixedValidInvalid() {
|
|
validToken, err := s.fixture.ValidToken(nil)
|
|
s.Require().NoError(err)
|
|
expiredToken, err := s.fixture.ExpiredToken()
|
|
s.Require().NoError(err)
|
|
|
|
const goroutines = 40
|
|
var wg sync.WaitGroup
|
|
validCount := int32(0)
|
|
expiredCount := int32(0)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
var token string
|
|
if idx%2 == 0 {
|
|
token = validToken
|
|
} else {
|
|
token = expiredToken
|
|
}
|
|
|
|
err := s.tOidc.VerifyToken(token)
|
|
if idx%2 == 0 {
|
|
if err == nil {
|
|
atomic.AddInt32(&validCount, 1)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
atomic.AddInt32(&expiredCount, 1)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
s.T().Logf("Valid passed: %d, Expired rejected: %d", validCount, expiredCount)
|
|
}
|
|
|
|
func TestConcurrencyEdgeCasesSuite(t *testing.T) {
|
|
suite.Run(t, new(ConcurrencyEdgeCasesSuite))
|
|
}
|