Files
go-jdenticon/internal/engine/security_memory_test.go
Kevin McIntyre d9e84812ff Initial release: Go Jdenticon library v0.1.0
- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
2026-01-03 23:41:48 -05:00

295 lines
8.3 KiB
Go

package engine
import (
"context"
"runtime"
"strings"
"testing"
"time"
"github.com/ungluedlabs/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)
}
}
})
}
}