mirror of
https://github.com/go-i2p/reseed-tools.git
synced 2025-12-20 09:15:53 -05:00
Add: comprehensive tests for token memory bounds (#3)
This commit is contained in:
209
reseed/server_tokens_test.go
Normal file
209
reseed/server_tokens_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package reseed
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Test for Bug #3: Unbounded Memory Growth in Acceptable Tokens (FIXED)
|
||||
func TestAcceptableTokensMemoryBounds(t *testing.T) {
|
||||
server := &Server{}
|
||||
|
||||
// Test 1: Verify tokens are cleaned up after expiration
|
||||
t.Run("ExpiredTokenCleanup", func(t *testing.T) {
|
||||
// Create some tokens and artificially age them
|
||||
server.acceptables = make(map[string]time.Time)
|
||||
oldTime := time.Now().Add(-5 * time.Minute) // Older than 4-minute expiry
|
||||
recentTime := time.Now()
|
||||
|
||||
server.acceptables["old_token_1"] = oldTime
|
||||
server.acceptables["old_token_2"] = oldTime
|
||||
server.acceptables["recent_token"] = recentTime
|
||||
|
||||
if len(server.acceptables) != 3 {
|
||||
t.Errorf("Expected 3 tokens initially, got %d", len(server.acceptables))
|
||||
}
|
||||
|
||||
// Trigger cleanup by calling Acceptable
|
||||
_ = server.Acceptable()
|
||||
|
||||
// Check that old tokens were cleaned up but recent one remains
|
||||
if len(server.acceptables) > 2 {
|
||||
t.Errorf("Expected at most 2 tokens after cleanup, got %d", len(server.acceptables))
|
||||
}
|
||||
|
||||
// Verify recent token still exists
|
||||
if _, exists := server.acceptables["recent_token"]; !exists {
|
||||
t.Error("Recent token should not have been cleaned up")
|
||||
}
|
||||
|
||||
// Verify old tokens were removed
|
||||
if _, exists := server.acceptables["old_token_1"]; exists {
|
||||
t.Error("Old token should have been cleaned up")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 2: Verify size-based eviction when too many tokens
|
||||
t.Run("SizeBasedEviction", func(t *testing.T) {
|
||||
server.acceptables = make(map[string]time.Time)
|
||||
|
||||
// Add more than 50 tokens
|
||||
for i := 0; i < 60; i++ {
|
||||
token := server.Acceptable()
|
||||
// Ensure each token has a slightly different timestamp
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
if token == "" {
|
||||
t.Error("Acceptable() should return a valid token")
|
||||
}
|
||||
}
|
||||
|
||||
// Should be limited to around 50 tokens due to eviction
|
||||
if len(server.acceptables) > 55 {
|
||||
t.Errorf("Expected token count to be limited, got %d", len(server.acceptables))
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Verify token validation works correctly
|
||||
t.Run("TokenValidation", func(t *testing.T) {
|
||||
server.acceptables = make(map[string]time.Time)
|
||||
|
||||
// Generate a token
|
||||
token := server.Acceptable()
|
||||
if token == "" {
|
||||
t.Fatal("Expected valid token")
|
||||
}
|
||||
|
||||
// Verify token is valid
|
||||
if !server.CheckAcceptable(token) {
|
||||
t.Error("Token should be valid immediately after creation")
|
||||
}
|
||||
|
||||
// Verify token is consumed (single-use)
|
||||
if server.CheckAcceptable(token) {
|
||||
t.Error("Token should not be valid after first use")
|
||||
}
|
||||
|
||||
// Verify invalid token returns false
|
||||
if server.CheckAcceptable("invalid_token") {
|
||||
t.Error("Invalid token should return false")
|
||||
}
|
||||
})
|
||||
|
||||
// Test 4: Verify memory doesn't grow unboundedly
|
||||
t.Run("UnboundedGrowthPrevention", func(t *testing.T) {
|
||||
server.acceptables = make(map[string]time.Time)
|
||||
|
||||
// Generate many tokens without checking them
|
||||
// This was the original bug scenario
|
||||
for i := 0; i < 200; i++ {
|
||||
_ = server.Acceptable()
|
||||
}
|
||||
|
||||
// Memory should be bounded
|
||||
if len(server.acceptables) > 60 {
|
||||
t.Errorf("Memory growth not properly bounded: %d tokens", len(server.acceptables))
|
||||
}
|
||||
|
||||
t.Logf("Token map size after 200 generations: %d (should be bounded)", len(server.acceptables))
|
||||
})
|
||||
|
||||
// Test 5: Test concurrent access safety
|
||||
t.Run("ConcurrentAccess", func(t *testing.T) {
|
||||
server.acceptables = make(map[string]time.Time)
|
||||
|
||||
// Launch multiple goroutines generating and checking tokens
|
||||
done := make(chan bool, 4)
|
||||
|
||||
// Token generators
|
||||
go func() {
|
||||
for i := 0; i < 50; i++ {
|
||||
_ = server.Acceptable()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 50; i++ {
|
||||
_ = server.Acceptable()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Token checkers
|
||||
go func() {
|
||||
for i := 0; i < 25; i++ {
|
||||
token := server.Acceptable()
|
||||
_ = server.CheckAcceptable(token)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 25; i++ {
|
||||
token := server.Acceptable()
|
||||
_ = server.CheckAcceptable(token)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 4; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Should not panic and should have bounded size
|
||||
if len(server.acceptables) > 100 {
|
||||
t.Errorf("Concurrent access resulted in unbounded growth: %d tokens", len(server.acceptables))
|
||||
}
|
||||
|
||||
t.Logf("Token map size after concurrent access: %d", len(server.acceptables))
|
||||
})
|
||||
}
|
||||
|
||||
// Test the cleanup methods directly
|
||||
func TestTokenCleanupMethods(t *testing.T) {
|
||||
server := &Server{
|
||||
acceptables: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
// Test cleanupExpiredTokensUnsafe
|
||||
t.Run("CleanupExpired", func(t *testing.T) {
|
||||
now := time.Now()
|
||||
server.acceptables["expired1"] = now.Add(-5 * time.Minute)
|
||||
server.acceptables["expired2"] = now.Add(-6 * time.Minute)
|
||||
server.acceptables["valid"] = now
|
||||
|
||||
server.cleanupExpiredTokensUnsafe()
|
||||
|
||||
if len(server.acceptables) != 1 {
|
||||
t.Errorf("Expected 1 token after cleanup, got %d", len(server.acceptables))
|
||||
}
|
||||
|
||||
if _, exists := server.acceptables["valid"]; !exists {
|
||||
t.Error("Valid token should remain after cleanup")
|
||||
}
|
||||
})
|
||||
|
||||
// Test evictOldestTokensUnsafe
|
||||
t.Run("EvictOldest", func(t *testing.T) {
|
||||
server.acceptables = make(map[string]time.Time)
|
||||
now := time.Now()
|
||||
|
||||
// Add tokens with different timestamps
|
||||
for i := 0; i < 10; i++ {
|
||||
server.acceptables[string(rune('a'+i))] = now.Add(time.Duration(-i) * time.Minute)
|
||||
}
|
||||
|
||||
// Evict to keep only 5
|
||||
server.evictOldestTokensUnsafe(5)
|
||||
|
||||
if len(server.acceptables) != 5 {
|
||||
t.Errorf("Expected 5 tokens after eviction, got %d", len(server.acceptables))
|
||||
}
|
||||
|
||||
// The newest tokens should remain
|
||||
if _, exists := server.acceptables["a"]; !exists {
|
||||
t.Error("Newest token should remain after eviction")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user