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
This commit is contained in:
294
internal/engine/security_memory_test.go
Normal file
294
internal/engine/security_memory_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user