php/src/Core/Tests/Feature/ImageOptimizerTest.php
Snider 28d004ff61
Some checks failed
CI / PHP 8.4 (push) Failing after 1m54s
CI / PHP 8.3 (push) Failing after 1m58s
feat: replace Go CLI with PHP framework
Go CLI commands moved to core/go-php. This repo now contains
the Laravel modular monolith framework (previously php-framework).

- Remove all Go files (now in core/go-php)
- Add PHP framework: event-driven module loading, lifecycle events
- Composer package: core/php
- core/php-framework remains as-is for backward compat

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-06 08:49:51 +00:00

466 lines
14 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
pest()->group('slow');
use Core\Media\Image\ImageOptimization;
use Core\Media\Image\ImageOptimizer;
use Core\Media\Image\OptimizationResult;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
beforeEach(function () {
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create();
$this->user->hostWorkspaces()->attach($this->workspace->id, ['is_default' => true]);
// Ensure storage directory exists
Storage::fake('local');
// Override config for testing
config([
'images.optimization.enabled' => true,
'images.optimization.driver' => 'gd',
'images.optimization.quality' => 80,
'images.optimization.png_compression' => 6,
'images.optimization.min_size_kb' => 10,
'images.optimization.max_size_mb' => 10,
]);
});
describe('ImageOptimizer service', function () {
it('can be instantiated', function () {
$optimizer = new ImageOptimizer;
expect($optimizer)->toBeInstanceOf(ImageOptimizer::class);
});
it('optimises JPEG images with GD driver', function () {
// Create a test JPEG image
$testImage = createTestJpegImage(800, 600);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
expect($result)->toBeInstanceOf(OptimizationResult::class);
expect($result->originalSize)->toBeGreaterThan(0);
expect($result->optimizedSize)->toBeGreaterThan(0);
expect($result->driver)->toBe('gd');
expect($result->path)->toBe($testImage);
// Clean up
unlink($testImage);
});
it('optimises PNG images with compression', function () {
// Create a test PNG image
$testImage = createTestPngImage(400, 300);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
expect($result)->toBeInstanceOf(OptimizationResult::class);
expect($result->originalSize)->toBeGreaterThan(0);
expect($result->optimizedSize)->toBeGreaterThan(0);
expect($result->driver)->toBe('gd');
// Clean up
unlink($testImage);
});
it('skips files smaller than minimum size', function () {
// Create a tiny image (less than 10KB)
$testImage = createTestJpegImage(50, 50);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
// Should return no-op result (no optimization)
expect($result->percentageSaved)->toBe(0);
// Clean up
unlink($testImage);
});
it('skips files larger than maximum size', function () {
// Set a very small max size for testing
config(['images.optimization.max_size_mb' => 0.001]); // 1KB
$testImage = createTestJpegImage(800, 600);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
// Should return no-op result
expect($result->percentageSaved)->toBe(0);
// Clean up
unlink($testImage);
});
it('throws exception for non-existent file', function () {
$optimizer = new ImageOptimizer;
$optimizer->optimize('/path/to/nonexistent/file.jpg');
})->throws(\InvalidArgumentException::class, 'File not found');
it('handles invalid image files gracefully', function () {
// Create a text file pretending to be an image
$testFile = sys_get_temp_dir().'/test_invalid.jpg';
file_put_contents($testFile, 'This is not an image');
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testFile);
// Service should return no-op result instead of throwing
expect($result)->toBeInstanceOf(OptimizationResult::class);
expect($result->percentageSaved)->toBe(0);
unlink($testFile);
});
it('respects custom quality settings', function () {
$testImage1 = createTestJpegImage(800, 600);
$testImage2 = createTestJpegImage(800, 600);
$optimizer = new ImageOptimizer;
// High quality (less compression)
$result1 = $optimizer->optimize($testImage1, ['quality' => 95]);
// Low quality (more compression)
$result2 = $optimizer->optimize($testImage2, ['quality' => 50]);
// Lower quality should result in smaller file
expect($result2->optimizedSize)->toBeLessThan($result1->optimizedSize);
// Clean up
unlink($testImage1);
unlink($testImage2);
});
it('handles WebP images', function () {
// Only test if WebP support is available
if (! function_exists('imagewebp')) {
$this->markTestSkipped('WebP support not available in GD');
}
$testImage = createTestWebpImage(400, 300);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
expect($result)->toBeInstanceOf(OptimizationResult::class);
expect($result->driver)->toBe('gd');
// Clean up
unlink($testImage);
});
it('can be disabled via config', function () {
config(['images.optimization.enabled' => false]);
$testImage = createTestJpegImage(800, 600);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
// Should return no-op result
expect($result->percentageSaved)->toBe(0);
expect($result->originalSize)->toBe($result->optimizedSize);
// Clean up
unlink($testImage);
});
});
describe('Uploaded file optimization', function () {
it('optimises uploaded files', function () {
// Create a test JPEG
$tempPath = createTestJpegImage(800, 600);
// Simulate UploadedFile
$uploadedFile = new UploadedFile($tempPath, 'test.jpg', 'image/jpeg', null, true);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimizeUploadedFile($uploadedFile);
expect($result)->toBeInstanceOf(OptimizationResult::class);
expect($result->originalSize)->toBeGreaterThan(0);
// Clean up
@unlink($tempPath);
});
});
describe('OptimizationResult value object', function () {
it('calculates bytes saved correctly', function () {
$result = new OptimizationResult(
originalSize: 100000,
optimizedSize: 65000,
percentageSaved: 35,
path: '/test/path.jpg',
driver: 'gd'
);
expect($result->bytesSaved())->toBe(35000);
});
it('reports success when savings exist', function () {
$result = new OptimizationResult(
originalSize: 100000,
optimizedSize: 80000,
percentageSaved: 20,
path: '/test/path.jpg',
driver: 'gd'
);
expect($result->wasSuccessful())->toBeTrue();
});
it('reports no success when no savings', function () {
$result = new OptimizationResult(
originalSize: 100000,
optimizedSize: 100000,
percentageSaved: 0,
path: '/test/path.jpg',
driver: 'gd'
);
expect($result->wasSuccessful())->toBeFalse();
});
it('generates human-readable summary', function () {
$result = new OptimizationResult(
originalSize: 120000,
optimizedSize: 66000,
percentageSaved: 45,
path: '/test/path.jpg',
driver: 'gd'
);
$summary = $result->getSummary();
expect($summary)->toContain('45%');
expect($summary)->toContain('KB');
});
it('converts to array', function () {
$result = new OptimizationResult(
originalSize: 100000,
optimizedSize: 80000,
percentageSaved: 20,
path: '/test/path.jpg',
driver: 'gd'
);
$array = $result->toArray();
expect($array)->toHaveKeys([
'original_size',
'optimized_size',
'percentage_saved',
'path',
'driver',
'bytes_saved',
]);
expect($array['bytes_saved'])->toBe(20000);
});
});
describe('ImageOptimization model', function () {
it('stores optimization record in database', function () {
$optimization = ImageOptimization::create([
'path' => '/storage/test.jpg',
'original_path' => null,
'original_size' => 150000,
'optimized_size' => 90000,
'percentage_saved' => 40,
'driver' => 'gd',
'quality' => 80,
'workspace_id' => $this->workspace->id,
]);
expect($optimization->exists)->toBeTrue();
expect($optimization->workspace_id)->toBe($this->workspace->id);
expect($optimization->percentage_saved)->toBe(40);
});
it('generates human-readable savings', function () {
$optimization = ImageOptimization::create([
'path' => '/storage/test.jpg',
'original_size' => 120000,
'optimized_size' => 66000,
'percentage_saved' => 45,
'driver' => 'gd',
'quality' => 80,
]);
$savings = $optimization->savings_human;
expect($savings)->toContain('45%');
expect($savings)->toContain('KB');
});
it('scopes to workspace', function () {
$workspace2 = Workspace::factory()->create();
// Create optimization for workspace 1
ImageOptimization::create([
'path' => '/storage/test1.jpg',
'original_size' => 100000,
'optimized_size' => 80000,
'percentage_saved' => 20,
'driver' => 'gd',
'quality' => 80,
'workspace_id' => $this->workspace->id,
]);
// Create optimization for workspace 2
ImageOptimization::create([
'path' => '/storage/test2.jpg',
'original_size' => 100000,
'optimized_size' => 80000,
'percentage_saved' => 20,
'driver' => 'gd',
'quality' => 80,
'workspace_id' => $workspace2->id,
]);
$workspace1Optimizations = ImageOptimization::forWorkspace($this->workspace)->get();
expect($workspace1Optimizations)->toHaveCount(1);
expect($workspace1Optimizations->first()->workspace_id)->toBe($this->workspace->id);
});
it('calculates workspace statistics', function () {
// Create multiple optimizations
ImageOptimization::create([
'path' => '/storage/test1.jpg',
'original_size' => 100000,
'optimized_size' => 80000,
'percentage_saved' => 20,
'driver' => 'gd',
'quality' => 80,
'workspace_id' => $this->workspace->id,
]);
ImageOptimization::create([
'path' => '/storage/test2.jpg',
'original_size' => 150000,
'optimized_size' => 90000,
'percentage_saved' => 40,
'driver' => 'gd',
'quality' => 80,
'workspace_id' => $this->workspace->id,
]);
$stats = ImageOptimization::getWorkspaceStats($this->workspace);
expect($stats['count'])->toBe(2);
expect($stats['total_original'])->toBe(250000);
expect($stats['total_optimized'])->toBe(170000);
expect($stats['total_saved'])->toBe(80000);
expect($stats['average_percentage'])->toBeGreaterThan(0);
});
});
describe('Statistics tracking', function () {
it('records optimization in database via service', function () {
$testImage = createTestJpegImage(800, 600);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
// Record the optimization
$record = $optimizer->recordOptimization(
$result,
$this->workspace
);
expect($record)->toBeInstanceOf(ImageOptimization::class);
expect($record->workspace_id)->toBe($this->workspace->id);
expect($record->driver)->toBe('gd');
// Clean up
unlink($testImage);
});
it('retrieves workspace stats via service', function () {
$testImage = createTestJpegImage(800, 600);
$optimizer = new ImageOptimizer;
$result = $optimizer->optimize($testImage);
$optimizer->recordOptimization($result, $this->workspace);
$stats = $optimizer->getStats($this->workspace);
expect($stats)->toBeArray();
expect($stats['count'])->toBe(1);
// Clean up
unlink($testImage);
});
});
// Helper functions
function createTestJpegImage(int $width, int $height): string
{
$image = imagecreatetruecolor($width, $height);
// Fill with some colours to make it realistic
$bgColor = imagecolorallocate($image, 100, 150, 200);
imagefill($image, 0, 0, $bgColor);
// Add some shapes for realistic compression
$shapeColor = imagecolorallocate($image, 255, 100, 50);
imagefilledrectangle($image, 50, 50, 150, 150, $shapeColor);
imagefilledellipse($image, 300, 200, 100, 100, $shapeColor);
$tempPath = sys_get_temp_dir().'/test_'.uniqid().'.jpg';
imagejpeg($image, $tempPath, 95); // High quality initially
imagedestroy($image);
return $tempPath;
}
function createTestPngImage(int $width, int $height): string
{
$image = imagecreatetruecolor($width, $height);
$bgColor = imagecolorallocate($image, 100, 150, 200);
imagefill($image, 0, 0, $bgColor);
$shapeColor = imagecolorallocate($image, 255, 100, 50);
imagefilledrectangle($image, 20, 20, 80, 80, $shapeColor);
$tempPath = sys_get_temp_dir().'/test_'.uniqid().'.png';
imagepng($image, $tempPath, 0); // No compression initially
imagedestroy($image);
return $tempPath;
}
function createTestWebpImage(int $width, int $height): string
{
$image = imagecreatetruecolor($width, $height);
$bgColor = imagecolorallocate($image, 100, 150, 200);
imagefill($image, 0, 0, $bgColor);
$tempPath = sys_get_temp_dir().'/test_'.uniqid().'.webp';
imagewebp($image, $tempPath, 95);
imagedestroy($image);
return $tempPath;
}