Files
Erupe/common/token/token_test.go
Houmgaor a7b0deaa42 fix: resolve data race in token.RNG global
Wrap *rand.Rand in a mutex-protected SafeRand type to make the global
RNG safe for concurrent use across goroutines. The previous bare
*rand.Rand caused data races detected by go test -race.
2026-02-16 18:19:18 +01:00

340 lines
7.5 KiB
Go

package token
import (
"testing"
"time"
)
func TestGenerate_Length(t *testing.T) {
tests := []struct {
name string
length int
}{
{"zero length", 0},
{"short", 5},
{"medium", 32},
{"long", 100},
{"very long", 1000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Generate(tt.length)
if len(result) != tt.length {
t.Errorf("Generate(%d) length = %d, want %d", tt.length, len(result), tt.length)
}
})
}
}
func TestGenerate_CharacterSet(t *testing.T) {
// Verify that generated tokens only contain alphanumeric characters
validChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
validCharMap := make(map[rune]bool)
for _, c := range validChars {
validCharMap[c] = true
}
token := Generate(1000) // Large sample
for _, c := range token {
if !validCharMap[c] {
t.Errorf("Generate() produced invalid character: %c", c)
}
}
}
func TestGenerate_Randomness(t *testing.T) {
// Generate multiple tokens and verify they're different
tokens := make(map[string]bool)
count := 100
length := 32
for i := 0; i < count; i++ {
token := Generate(length)
if tokens[token] {
t.Errorf("Generate() produced duplicate token: %s", token)
}
tokens[token] = true
}
if len(tokens) != count {
t.Errorf("Generated %d unique tokens, want %d", len(tokens), count)
}
}
func TestGenerate_ContainsUppercase(t *testing.T) {
// With enough characters, should contain at least one uppercase letter
token := Generate(1000)
hasUpper := false
for _, c := range token {
if c >= 'A' && c <= 'Z' {
hasUpper = true
break
}
}
if !hasUpper {
t.Error("Generate(1000) should contain at least one uppercase letter")
}
}
func TestGenerate_ContainsLowercase(t *testing.T) {
// With enough characters, should contain at least one lowercase letter
token := Generate(1000)
hasLower := false
for _, c := range token {
if c >= 'a' && c <= 'z' {
hasLower = true
break
}
}
if !hasLower {
t.Error("Generate(1000) should contain at least one lowercase letter")
}
}
func TestGenerate_ContainsDigit(t *testing.T) {
// With enough characters, should contain at least one digit
token := Generate(1000)
hasDigit := false
for _, c := range token {
if c >= '0' && c <= '9' {
hasDigit = true
break
}
}
if !hasDigit {
t.Error("Generate(1000) should contain at least one digit")
}
}
func TestGenerate_Distribution(t *testing.T) {
// Test that characters are reasonably distributed
token := Generate(6200) // 62 chars * 100 = good sample size
charCount := make(map[rune]int)
for _, c := range token {
charCount[c]++
}
// With 62 valid characters and 6200 samples, average should be 100 per char
// We'll accept a range to account for randomness
minExpected := 50 // Allow some variance
maxExpected := 150
for c, count := range charCount {
if count < minExpected || count > maxExpected {
t.Logf("Character %c appeared %d times (outside expected range %d-%d)", c, count, minExpected, maxExpected)
}
}
// Just verify we have a good spread of characters
if len(charCount) < 50 {
t.Errorf("Only %d different characters used, want at least 50", len(charCount))
}
}
func TestNewSafeRand(t *testing.T) {
rng := NewSafeRand()
if rng == nil {
t.Fatal("NewSafeRand() returned nil")
}
// Test that it produces different values on subsequent calls
val1 := rng.Intn(1000000)
val2 := rng.Intn(1000000)
if val1 == val2 {
// This is possible but unlikely, let's try a few more times
same := true
for i := 0; i < 10; i++ {
if rng.Intn(1000000) != val1 {
same = false
break
}
}
if same {
t.Error("NewSafeRand() produced same value 12 times in a row")
}
}
}
func TestRNG_GlobalVariable(t *testing.T) {
// Test that the global RNG variable is initialized
if RNG == nil {
t.Fatal("Global RNG is nil")
}
// Test that it works
val := RNG.Intn(100)
if val < 0 || val >= 100 {
t.Errorf("RNG.Intn(100) = %d, out of range [0, 100)", val)
}
}
func TestRNG_Uint32(t *testing.T) {
// Test that RNG can generate uint32 values
val1 := RNG.Uint32()
val2 := RNG.Uint32()
// They should be different (with very high probability)
if val1 == val2 {
// Try a few more times
same := true
for i := 0; i < 10; i++ {
if RNG.Uint32() != val1 {
same = false
break
}
}
if same {
t.Error("RNG.Uint32() produced same value 12 times")
}
}
}
func TestGenerate_Concurrency(t *testing.T) {
// Test that Generate works correctly when called concurrently
done := make(chan string, 100)
for i := 0; i < 100; i++ {
go func() {
token := Generate(32)
done <- token
}()
}
tokens := make(map[string]bool)
for i := 0; i < 100; i++ {
token := <-done
if len(token) != 32 {
t.Errorf("Token length = %d, want 32", len(token))
}
tokens[token] = true
}
// Should have many unique tokens (allow some small chance of duplicates)
if len(tokens) < 95 {
t.Errorf("Only %d unique tokens from 100 concurrent calls", len(tokens))
}
}
func TestGenerate_EmptyString(t *testing.T) {
token := Generate(0)
if token != "" {
t.Errorf("Generate(0) = %q, want empty string", token)
}
}
func TestGenerate_OnlyAlphanumeric(t *testing.T) {
// Verify no special characters
token := Generate(1000)
for i, c := range token {
isValid := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
if !isValid {
t.Errorf("Token[%d] = %c (invalid character)", i, c)
}
}
}
func TestNewSafeRand_DifferentSeeds(t *testing.T) {
// Create two RNGs at different times and verify they produce different sequences
rng1 := NewSafeRand()
time.Sleep(1 * time.Millisecond) // Ensure different seed
rng2 := NewSafeRand()
val1 := rng1.Intn(1000000)
val2 := rng2.Intn(1000000)
// They should be different with high probability
if val1 == val2 {
// Try again
val1 = rng1.Intn(1000000)
val2 = rng2.Intn(1000000)
if val1 == val2 {
t.Log("Two RNGs created at different times produced same first two values (possible but unlikely)")
}
}
}
func BenchmarkGenerate_Short(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Generate(8)
}
}
func BenchmarkGenerate_Medium(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Generate(32)
}
}
func BenchmarkGenerate_Long(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Generate(128)
}
}
func BenchmarkNewSafeRand(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = NewSafeRand()
}
}
func BenchmarkRNG_Intn(b *testing.B) {
rng := NewSafeRand()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = rng.Intn(62)
}
}
func BenchmarkRNG_Uint32(b *testing.B) {
rng := NewSafeRand()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = rng.Uint32()
}
}
func TestGenerate_ConsistentCharacterSet(t *testing.T) {
// Verify the character set matches what's defined in the code
expectedChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if len(expectedChars) != 62 {
t.Errorf("Expected character set length = %d, want 62", len(expectedChars))
}
// Count each type
lowercase := 0
uppercase := 0
digits := 0
for _, c := range expectedChars {
if c >= 'a' && c <= 'z' {
lowercase++
} else if c >= 'A' && c <= 'Z' {
uppercase++
} else if c >= '0' && c <= '9' {
digits++
}
}
if lowercase != 26 {
t.Errorf("Lowercase count = %d, want 26", lowercase)
}
if uppercase != 26 {
t.Errorf("Uppercase count = %d, want 26", uppercase)
}
if digits != 10 {
t.Errorf("Digits count = %d, want 10", digits)
}
}
func TestRNG_Type(t *testing.T) {
// Verify RNG is of type *SafeRand
var _ *SafeRand = RNG
var _ *SafeRand = NewSafeRand()
}