feat(cms,asset): add comprehensive test suite and finalize modules

- Add comprehensive test suite for CMS and Asset modules using Pest Framework
- Implement ContentTypeService::delete() protection against deletion of in-use content types
- Add CannotDeleteContentTypeInUseException for better error handling
- Fix DerivatPipelineRegistry::getAllPipelines() to handle object uniqueness correctly
- Fix VariantName::getScale() to correctly parse scales with file extensions
- Update CMS module documentation with new features, exceptions, and test coverage
- Add CmsTestHelpers and AssetTestHelpers for test data factories
- Fix BlockTypeRegistry to be immutable after construction
- Update ContentTypeService to check for associated content before deletion
- Improve BlockRendererRegistry initialization

Test coverage:
- Value Objects: All CMS and Asset value objects
- Services: ContentService, ContentTypeService, SlugGenerator, BlockValidator, ContentLocalizationService, AssetService, DeduplicationService, MetadataExtractor
- Repositories: All database repositories with mocked connections
- Rendering: Block renderers and ContentRenderer
- Controllers: API endpoints for both modules

254 tests passing, 38 remaining (mostly image processing pipeline tests)
This commit is contained in:
2025-11-10 02:12:28 +01:00
parent 74d50a29cc
commit 2d53270056
53 changed files with 5699 additions and 15 deletions

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
use App\Domain\Asset\Entities\Asset;
use App\Domain\Asset\Entities\AssetVariant;
use App\Domain\Asset\Pipeline\ImageDerivatPipeline;
use App\Domain\Asset\Storage\AssetStorageInterface;
use App\Domain\Asset\ValueObjects\AssetId;
use App\Domain\Asset\ValueObjects\AssetMetadata;
use App\Framework\Core\ValueObjects\FileSize;
use App\Framework\Core\ValueObjects\Hash;
use App\Framework\Core\ValueObjects\HashAlgorithm;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\SystemClock;
use App\Framework\Http\MimeType;
use App\Framework\Storage\ValueObjects\BucketName;
use App\Framework\Storage\ValueObjects\ObjectKey;
use Tests\Support\AssetTestHelpers;
describe('ImageDerivatPipeline', function () {
beforeEach(function () {
$this->clock = new SystemClock();
$this->variantBucket = BucketName::fromString('variants');
$this->pipeline = new ImageDerivatPipeline($this->variantBucket);
$this->storage = Mockery::mock(AssetStorageInterface::class);
});
it('returns empty array for non-image assets', function () {
$asset = AssetTestHelpers::createAsset($this->clock, mime: MimeType::VIDEO_MP4);
$content = 'video-content';
$variants = $this->pipeline->process($asset, $content, $this->storage);
expect($variants)->toBe([]);
});
it('generates variants for JPEG image', function () {
$asset = $this->createTestImageAsset(1920, 1080, MimeType::IMAGE_JPEG);
$content = $this->createTestImageContent(1920, 1080);
$this->storage->shouldReceive('put')
->atLeast()->once()
->andReturnNull();
$variants = $this->pipeline->process($asset, $content, $this->storage);
expect($variants)->toBeArray();
expect(count($variants))->toBeGreaterThan(0);
});
it('generates responsive variants', function () {
$asset = $this->createTestImageAsset(1920, 1080);
$content = $this->createTestImageContent(1920, 1080);
$this->storage->shouldReceive('put')
->atLeast()->once()
->andReturnNull();
$variants = $this->pipeline->process($asset, $content, $this->storage);
$variantNames = array_map(fn ($v) => $v->variant->toString(), $variants);
expect($variantNames)->toContain('1200w');
expect($variantNames)->toContain('800w');
});
it('generates thumbnail variants', function () {
$asset = $this->createTestImageAsset(1920, 1080);
$content = $this->createTestImageContent(1920, 1080);
$this->storage->shouldReceive('put')
->atLeast()->once()
->andReturnNull();
$variants = $this->pipeline->process($asset, $content, $this->storage);
$variantNames = array_map(fn ($v) => $v->variant->toString(), $variants);
expect($variantNames)->toContain('thumb@1x');
expect($variantNames)->toContain('thumb@2x');
});
it('creates variants with correct dimensions', function () {
$asset = $this->createTestImageAsset(1920, 1080);
$content = $this->createTestImageContent(1920, 1080);
$this->storage->shouldReceive('put')
->atLeast()->once()
->andReturnNull();
$variants = $this->pipeline->process($asset, $content, $this->storage);
foreach ($variants as $variant) {
expect($variant->meta->getWidth())->not->toBeNull();
expect($variant->meta->getHeight())->not->toBeNull();
}
});
it('creates variants in variant bucket', function () {
$asset = $this->createTestImageAsset(1920, 1080);
$content = $this->createTestImageContent(1920, 1080);
$this->storage->shouldReceive('put')
->atLeast()->once()
->andReturnNull();
$variants = $this->pipeline->process($asset, $content, $this->storage);
foreach ($variants as $variant) {
expect($variant->bucket->toString())->toBe('variants');
}
});
it('creates variants as WebP format', function () {
$asset = $this->createTestImageAsset(1920, 1080);
$content = $this->createTestImageContent(1920, 1080);
$this->storage->shouldReceive('put')
->atLeast()->once()
->andReturnNull();
$variants = $this->pipeline->process($asset, $content, $this->storage);
foreach ($variants as $variant) {
expect($variant->mime)->toBe(MimeType::IMAGE_WEBP);
}
});
it('returns supported formats', function () {
$formats = $this->pipeline->getSupportedFormats();
expect($formats)->toContain('image/jpeg');
expect($formats)->toContain('image/png');
expect($formats)->toContain('image/gif');
expect($formats)->toContain('image/webp');
});
it('handles invalid image content gracefully', function () {
$asset = $this->createTestImageAsset(1920, 1080);
$content = 'invalid-image-content';
$variants = $this->pipeline->process($asset, $content, $this->storage);
// Should return empty array or filter out nulls
expect($variants)->toBeArray();
});
function createTestImageAsset(int $width, int $height, MimeType $mime = null): Asset
{
$mime = $mime ?? MimeType::IMAGE_JPEG;
return new Asset(
id: AssetId::generate($this->clock),
bucket: BucketName::fromString('media'),
key: ObjectKey::fromString('orig/2025/01/15/test.jpg'),
mime: $mime,
bytes: FileSize::fromBytes(1024),
sha256: Hash::create('test', HashAlgorithm::SHA256),
meta: AssetMetadata::empty()->withDimensions($width, $height),
createdAt: Timestamp::now()
);
}
function createTestImageContent(int $width, int $height): string
{
$img = imagecreatetruecolor($width, $height);
ob_start();
imagejpeg($img);
$content = ob_get_clean();
imagedestroy($img);
return $content;
}
});