package performance import ( "context" "crypto/rand" "encoding/hex" "testing" "time" "github.com/user-management-system/internal/auth" "github.com/user-management-system/internal/cache" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/repository" "golang.org/x/crypto/argon2" ) // ============================================================================= // Password Hashing Benchmarks (Argon2id) // ============================================================================= func BenchmarkArgon2idHashing(b *testing.B) { password := []byte("TestPassword123!") salt := make([]byte, 16) rand.Read(salt) b.ResetTimer() for i := 0; i < b.N; i++ { _ = argon2.IDKey(password, salt, 5, 64*1024, 4, 32) } } func BenchmarkArgon2idHashingParallel(b *testing.B) { password := []byte("TestPassword123!") salt := make([]byte, 16) rand.Read(salt) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { localSalt := make([]byte, 16) rand.Read(localSalt) for pb.Next() { _ = argon2.IDKey(password, localSalt, 5, 64*1024, 4, 32) } }) } func BenchmarkArgon2idHashingDefaultParams(b *testing.B) { password := []byte("TestPassword123!") // Default params from our config: time=5, memory=64MB, threads=4 salt := make([]byte, 16) rand.Read(salt) b.ResetTimer() for i := 0; i < b.N; i++ { _ = argon2.IDKey(password, salt, 5, 64*1024, 4, 32) } b.ReportMetric(64.0, "memory_MB") b.ReportMetric(5.0, "time_ops") b.ReportMetric(4.0, "threads") } // ============================================================================= // JWT Benchmarks // ============================================================================= func BenchmarkJWTGenerateToken(b *testing.B) { jwtManager, _ := auth.NewJWT("benchmark-secret-key-32bytes!", 2*time.Hour, 7*24*time.Hour) b.ResetTimer() for i := 0; i < b.N; i++ { _, _, _ = jwtManager.GenerateTokenPair(int64(i), "testuser", 0) } } func BenchmarkJWTValidateToken(b *testing.B) { jwtManager, _ := auth.NewJWT("benchmark-secret-key-32bytes!", 2*time.Hour, 7*24*time.Hour) token, _, _ := jwtManager.GenerateTokenPair(1, "testuser", 0) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = jwtManager.ValidateAccessToken(token) } } func BenchmarkJWTGenerateAndValidate(b *testing.B) { jwtManager, _ := auth.NewJWT("benchmark-secret-key-32bytes!", 2*time.Hour, 7*24*time.Hour) b.ResetTimer() for i := 0; i < b.N; i++ { token, _, _ := jwtManager.GenerateTokenPair(int64(i), "testuser", 0) jwtManager.ValidateAccessToken(token) } } // ============================================================================= // TOTP Benchmarks // ============================================================================= func BenchmarkTOTPGenerateSecret(b *testing.B) { totpManager := auth.NewTOTPManager() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = totpManager.GenerateSecret("testuser") } } func BenchmarkTOTPGenerateCurrentCode(b *testing.B) { totpManager := auth.NewTOTPManager() secret := make([]byte, 20) rand.Read(secret) _ = secret // Use the secret b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = totpManager.GenerateCurrentCode(base32StdSecret()) } } func base32StdSecret() string { b := make([]byte, 20) rand.Read(b) return "JBSWY3DPEHPK3PXP" // Example base32 secret } // ============================================================================= // Recovery Code Benchmarks // ============================================================================= func BenchmarkRecoveryCodeHashing(b *testing.B) { codes := generateTestCodes(10) b.ResetTimer() for i := 0; i < b.N; i++ { for _, code := range codes { _, _ = auth.HashRecoveryCode(code) } } } func BenchmarkRecoveryCodeVerification(b *testing.B) { codes := generateTestCodes(10) hashedCodes := make([]string, len(codes)) for i, code := range codes { h, _ := auth.HashRecoveryCode(code) hashedCodes[i] = h } testCode := codes[5] // Use the 6th code b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = auth.VerifyRecoveryCode(testCode, hashedCodes) } } func generateTestCodes(count int) []string { codes := make([]string, count) for i := 0; i < count; i++ { b := make([]byte, RecoveryCodeLength*2) rand.Read(b) encoded := base32Encode(b) codes[i] = formatRecoveryCode(encoded[:10]) } return codes } const RecoveryCodeLength = 10 // from totp.go func base32Encode(b []byte) string { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" result := make([]byte, (len(b)*8+4)/5) for i := 0; i < len(result); i++ { var val uint32 var bits int for j := 0; j < 5 && i*5+j < len(b)*8; j++ { if bits < 5 { val = (val << bits) | uint32(b[i*5/8]>>(8-bits))&0xFF bits += 8 } } result[i] = alphabet[(val>>(bits-5))&0x1F] } return string(result) } func formatRecoveryCode(s string) string { if len(s) >= 10 { return s[:5] + "-" + s[5:10] } return s } // ============================================================================= // Cache Benchmarks // ============================================================================= func BenchmarkL1CacheGet(b *testing.B) { l1Cache := cache.NewL1Cache() l1Cache.Set("test-key", "test-value", 10*time.Minute) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = l1Cache.Get("test-key") } } func BenchmarkL1CacheSet(b *testing.B) { l1Cache := cache.NewL1Cache() b.ResetTimer() for i := 0; i < b.N; i++ { key := "test-key-" + hex.EncodeToString([]byte{byte(i)}) l1Cache.Set(key, "test-value", 10*time.Minute) } } func BenchmarkL1CacheGetMiss(b *testing.B) { l1Cache := cache.NewL1Cache() b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = l1Cache.Get("non-existent-key") } } // ============================================================================= // Database Benchmarks // ============================================================================= func BenchmarkUserRepositoryCreate(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewUserRepository(db) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { user := &domain.User{ Username: "benchuser" + hex.EncodeToString([]byte{byte(i)}), Email: domain.StrPtr("bench@example.com"), Password: "hash", Status: domain.UserStatusActive, } repo.Create(ctx, user) } } func BenchmarkUserRepositoryGetByID(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewUserRepository(db) ctx := context.Background() // Pre-create user user := &domain.User{ Username: "benchuser", Email: domain.StrPtr("bench@example.com"), Password: "hash", Status: domain.UserStatusActive, } repo.Create(ctx, user) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = repo.GetByID(ctx, user.ID) } } func BenchmarkUserRepositoryGetByUsername(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewUserRepository(db) ctx := context.Background() // Pre-create user user := &domain.User{ Username: "benchuser", Email: domain.StrPtr("bench@example.com"), Password: "hash", Status: domain.UserStatusActive, } repo.Create(ctx, user) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = repo.GetByUsername(ctx, "benchuser") } } func BenchmarkUserRepositoryList(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewUserRepository(db) ctx := context.Background() // Pre-create users for i := 0; i < 100; i++ { user := &domain.User{ Username: "benchuser" + hex.EncodeToString([]byte{byte(i)}), Email: domain.StrPtr("bench@example.com"), Password: "hash", Status: domain.UserStatusActive, } repo.Create(ctx, user) } b.ResetTimer() for i := 0; i < b.N; i++ { _, _, _ = repo.List(ctx, 0, 100) } } func BenchmarkUserRepositoryUpdate(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewUserRepository(db) ctx := context.Background() // Pre-create user user := &domain.User{ Username: "benchuser", Email: domain.StrPtr("bench@example.com"), Password: "hash", Status: domain.UserStatusActive, } repo.Create(ctx, user) b.ResetTimer() for i := 0; i < b.N; i++ { user.Nickname = "Updated Nickname" repo.Update(ctx, user) } } func BenchmarkRoleRepositoryCreate(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewRoleRepository(db) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { role := &domain.Role{ Name: "benchrole" + hex.EncodeToString([]byte{byte(i)}), Code: "benchrole" + hex.EncodeToString([]byte{byte(i)}), } repo.Create(ctx, role) } } // HMAC benchmarks removed - ComputeHMAC is not exported from auth package // ConstantTimeCompare benchmarks removed - it's internal to the auth package // ============================================================================= // Concurrency Stress Tests // ============================================================================= func BenchmarkConcurrentUserCreation(b *testing.B) { db := setupBenchmarkDB(b) repo := repository.NewUserRepository(db) ctx := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { user := &domain.User{ Username: "benchuser" + hex.EncodeToString([]byte{byte(i % 256)}), Email: domain.StrPtr("bench@example.com"), Password: "hash", Status: domain.UserStatusActive, } repo.Create(ctx, user) i++ } }) } func BenchmarkConcurrentCacheAccess(b *testing.B) { l1Cache := cache.NewL1Cache() // Pre-populate cache for i := 0; i < 100; i++ { key := "test-key-" + hex.EncodeToString([]byte{byte(i)}) l1Cache.Set(key, "test-value", 10*time.Minute) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { key := "test-key-" + hex.EncodeToString([]byte{byte(i % 100)}) l1Cache.Get(key) i++ } }) } // ============================================================================= // Helper function - setupBenchmarkDB is defined in performance_test.go // =============================================================================