- 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
295 lines
8.3 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|