package engine import ( "context" "runtime" "strings" "testing" "time" "gitea.dockr.co/kev/go-jdenticon/internal/constants" ) // TestResourceExhaustionProtection tests that the generator properly blocks // attempts to create extremely large icons that could cause memory exhaustion. func TestResourceExhaustionProtection(t *testing.T) { tests := []struct { name string maxIconSize int requestedSize float64 expectError bool errorContains string }{ { name: "valid size within default limit", maxIconSize: 0, // Use default requestedSize: 1024, expectError: false, }, { name: "valid size at exact default limit", maxIconSize: 0, // Use default requestedSize: constants.DefaultMaxIconSize, expectError: false, }, { name: "invalid size exceeds default limit by 1", maxIconSize: 0, // Use default requestedSize: constants.DefaultMaxIconSize + 1, expectError: true, errorContains: "exceeds maximum allowed size", }, { name: "extremely large size should be blocked", maxIconSize: 0, // Use default requestedSize: 100000, expectError: true, errorContains: "exceeds maximum allowed size", }, { name: "custom limit - valid size", maxIconSize: 1000, requestedSize: 1000, expectError: false, }, { name: "custom limit - invalid size", maxIconSize: 1000, requestedSize: 1001, expectError: true, errorContains: "exceeds maximum allowed size", }, { name: "disabled limit allows oversized requests", maxIconSize: -1, // Disabled requestedSize: constants.DefaultMaxIconSize + 1000, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := DefaultGeneratorConfig() config.MaxIconSize = tt.maxIconSize generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("Failed to create generator: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Use a simple hash for testing testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b" icon, err := generator.Generate(ctx, testHash, tt.requestedSize) if tt.expectError { if err == nil { t.Errorf("Expected error for size %f, but got none", tt.requestedSize) return } if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err) } if icon != nil { t.Errorf("Expected nil icon when error occurs, but got non-nil") } } else { if err != nil { t.Errorf("Unexpected error for size %f: %v", tt.requestedSize, err) return } if icon == nil { t.Errorf("Expected non-nil icon for valid size %f", tt.requestedSize) } } }) } } // TestMemoryUsageDoesNotSpikeOnRejection verifies that memory usage doesn't // spike when oversized icon requests are rejected, proving that the validation // happens before any memory allocation. func TestMemoryUsageDoesNotSpikeOnRejection(t *testing.T) { generator, err := NewGeneratorWithConfig(DefaultGeneratorConfig()) if err != nil { t.Fatalf("Failed to create generator: %v", err) } // Force garbage collection and get baseline memory stats runtime.GC() runtime.GC() // Run twice to ensure clean baseline var m1 runtime.MemStats runtime.ReadMemStats(&m1) baselineAlloc := m1.Alloc ctx := context.Background() testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b" // Attempt to generate an extremely large icon (should be rejected) oversizedRequest := float64(constants.DefaultMaxIconSize * 10) // 10x the limit icon, err := generator.Generate(ctx, testHash, oversizedRequest) // Verify the request was properly rejected if err == nil { t.Fatalf("Expected error for oversized request, but got none") } if icon != nil { t.Fatalf("Expected nil icon for oversized request, but got non-nil") } if !strings.Contains(err.Error(), "exceeds maximum allowed size") { t.Fatalf("Expected specific error message, got: %v", err) } // Check memory usage after the rejected request runtime.GC() var m2 runtime.MemStats runtime.ReadMemStats(&m2) postRejectionAlloc := m2.Alloc // Calculate memory increase (allow for some variance due to test overhead) memoryIncrease := postRejectionAlloc - baselineAlloc maxAcceptableIncrease := uint64(1024 * 1024) // 1MB tolerance for test overhead if memoryIncrease > maxAcceptableIncrease { t.Errorf("Memory usage spiked by %d bytes after rejection (baseline: %d, post: %d). "+ "This suggests memory allocation occurred before validation.", memoryIncrease, baselineAlloc, postRejectionAlloc) } t.Logf("Memory baseline: %d bytes, post-rejection: %d bytes, increase: %d bytes", baselineAlloc, postRejectionAlloc, memoryIncrease) } // TestConfigurationDefaults verifies that the default MaxIconSize is properly applied // when not explicitly set in the configuration. func TestConfigurationDefaults(t *testing.T) { tests := []struct { name string configSize int expectedMax int }{ { name: "zero config uses default", configSize: 0, expectedMax: constants.DefaultMaxIconSize, }, { name: "other negative config uses default", configSize: -5, expectedMax: constants.DefaultMaxIconSize, }, { name: "custom config is respected", configSize: 2000, expectedMax: 2000, }, { name: "disabled config is respected", configSize: -1, expectedMax: -1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := DefaultGeneratorConfig() config.MaxIconSize = tt.configSize generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("Failed to create generator: %v", err) } // Check that the effective max size was set correctly if generator.maxIconSize != tt.expectedMax { t.Errorf("Expected maxIconSize to be %d, but got %d", tt.expectedMax, generator.maxIconSize) } // Verify the limit is enforced (skip if disabled) if tt.expectedMax > 0 { ctx := context.Background() testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b" // Try a size just over the limit oversizedRequest := float64(tt.expectedMax + 1) icon, err := generator.Generate(ctx, testHash, oversizedRequest) if err == nil { t.Errorf("Expected error for size %f (limit: %d), but got none", oversizedRequest, tt.expectedMax) } if icon != nil { t.Errorf("Expected nil icon for oversized request") } } else if tt.expectedMax == -1 { // Test that disabled limit allows large sizes ctx := context.Background() testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b" // Try a very large size that would normally be blocked largeRequest := float64(constants.DefaultMaxIconSize + 1000) icon, err := generator.Generate(ctx, testHash, largeRequest) if err != nil { t.Errorf("Unexpected error for large size with disabled limit: %v", err) } if icon == nil { t.Errorf("Expected non-nil icon for large size with disabled limit") } } }) } } // TestBoundaryConditions tests edge cases around the size limit boundaries func TestBoundaryConditions(t *testing.T) { config := DefaultGeneratorConfig() config.MaxIconSize = 1000 generator, err := NewGeneratorWithConfig(config) if err != nil { t.Fatalf("Failed to create generator: %v", err) } ctx := context.Background() testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b" tests := []struct { name string size float64 expectError bool }{ {"size at exact limit", 1000, false}, {"size just under limit", 999, false}, {"size just over limit", 1001, true}, {"floating point at limit", 1000.0, false}, {"floating point just over", 1001.0, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { icon, err := generator.Generate(ctx, testHash, tt.size) if tt.expectError { if err == nil { t.Errorf("Expected error for size %f, but got none", tt.size) } if icon != nil { t.Errorf("Expected nil icon for oversized request") } } else { if err != nil { t.Errorf("Unexpected error for size %f: %v", tt.size, err) } if icon == nil { t.Errorf("Expected non-nil icon for valid size %f", tt.size) } } }) } }