- 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)
338 lines
11 KiB
PHP
338 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Domain\Asset\Commands\ProcessDerivatCommand;
|
|
use App\Domain\Asset\Entities\Asset;
|
|
use App\Domain\Asset\Entities\AssetTag;
|
|
use App\Domain\Asset\Entities\AssetVariant;
|
|
use App\Domain\Asset\Exceptions\AssetNotFoundException;
|
|
use App\Domain\Asset\Repositories\AssetRepository;
|
|
use App\Domain\Asset\Repositories\AssetTagRepository;
|
|
use App\Domain\Asset\Repositories\AssetVariantRepository;
|
|
use App\Domain\Asset\Services\AssetService;
|
|
use App\Domain\Asset\Services\DeduplicationService;
|
|
use App\Domain\Asset\Services\MetadataExtractor;
|
|
use App\Domain\Asset\Storage\AssetStorageInterface;
|
|
use App\Domain\Asset\ValueObjects\AssetId;
|
|
use App\Domain\Asset\ValueObjects\AssetMetadata;
|
|
use App\Domain\Asset\ValueObjects\VariantName;
|
|
use App\Framework\CommandBus\CommandBus;
|
|
use App\Framework\Core\ValueObjects\FileSize;
|
|
use App\Framework\Core\ValueObjects\Hash;
|
|
use App\Framework\Core\ValueObjects\HashAlgorithm;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Http\MimeType;
|
|
use App\Framework\Storage\ValueObjects\BucketName;
|
|
|
|
describe('AssetService', function () {
|
|
beforeEach(function () {
|
|
$this->clock = new SystemClock();
|
|
$this->assetRepository = Mockery::mock(AssetRepository::class);
|
|
$this->variantRepository = Mockery::mock(AssetVariantRepository::class);
|
|
$this->tagRepository = Mockery::mock(AssetTagRepository::class);
|
|
$this->storage = Mockery::mock(AssetStorageInterface::class);
|
|
$this->deduplicationService = Mockery::mock(DeduplicationService::class);
|
|
$this->metadataExtractor = Mockery::mock(MetadataExtractor::class);
|
|
$this->commandBus = Mockery::mock(CommandBus::class);
|
|
$this->service = new AssetService(
|
|
$this->assetRepository,
|
|
$this->variantRepository,
|
|
$this->tagRepository,
|
|
$this->storage,
|
|
$this->deduplicationService,
|
|
$this->metadataExtractor,
|
|
$this->commandBus,
|
|
$this->clock
|
|
);
|
|
});
|
|
|
|
it('uploads new asset successfully', function () {
|
|
$content = 'test-image-content';
|
|
$bucket = BucketName::fromString('media');
|
|
$mime = MimeType::IMAGE_JPEG;
|
|
$hash = Hash::create($content, HashAlgorithm::SHA256);
|
|
$meta = AssetMetadata::empty()->withDimensions(100, 200);
|
|
|
|
$this->deduplicationService->shouldReceive('checkDuplicate')
|
|
->once()
|
|
->with($hash)
|
|
->andReturn(null);
|
|
|
|
$this->metadataExtractor->shouldReceive('extractImageMetadata')
|
|
->once()
|
|
->with($content)
|
|
->andReturn($meta);
|
|
|
|
$this->storage->shouldReceive('put')
|
|
->once()
|
|
->andReturnNull();
|
|
|
|
$this->assetRepository->shouldReceive('save')
|
|
->once()
|
|
->andReturnNull();
|
|
|
|
$this->commandBus->shouldReceive('dispatch')
|
|
->once()
|
|
->with(Mockery::type(ProcessDerivatCommand::class))
|
|
->andReturnNull();
|
|
|
|
$asset = $this->service->upload($content, $bucket, $mime);
|
|
|
|
expect($asset)->toBeInstanceOf(Asset::class);
|
|
expect($asset->bucket->toString())->toBe('media');
|
|
expect($asset->mime)->toBe($mime);
|
|
});
|
|
|
|
it('returns existing asset when duplicate found', function () {
|
|
$content = 'test-image-content';
|
|
$bucket = BucketName::fromString('media');
|
|
$mime = MimeType::IMAGE_JPEG;
|
|
$hash = Hash::create($content, HashAlgorithm::SHA256);
|
|
$existingAsset = AssetTestHelpers::createAsset($this->clock);
|
|
|
|
$this->deduplicationService->shouldReceive('checkDuplicate')
|
|
->once()
|
|
->with($hash)
|
|
->andReturn($existingAsset);
|
|
|
|
$asset = $this->service->upload($content, $bucket, $mime);
|
|
|
|
expect($asset)->toBe($existingAsset);
|
|
});
|
|
|
|
it('adds tags to existing asset when duplicate found', function () {
|
|
$content = 'test-image-content';
|
|
$bucket = BucketName::fromString('media');
|
|
$mime = MimeType::IMAGE_JPEG;
|
|
$hash = Hash::create($content, HashAlgorithm::SHA256);
|
|
$existingAsset = AssetTestHelpers::createAsset($this->clock);
|
|
$tags = ['hero', 'landing-page'];
|
|
|
|
$this->deduplicationService->shouldReceive('checkDuplicate')
|
|
->once()
|
|
->with($hash)
|
|
->andReturn($existingAsset);
|
|
|
|
$this->tagRepository->shouldReceive('addTags')
|
|
->once()
|
|
->with($existingAsset->id, $tags)
|
|
->andReturnNull();
|
|
|
|
$asset = $this->service->upload($content, $bucket, $mime, null, $tags);
|
|
|
|
expect($asset)->toBe($existingAsset);
|
|
});
|
|
|
|
it('adds tags to new asset', function () {
|
|
$content = 'test-image-content';
|
|
$bucket = BucketName::fromString('media');
|
|
$mime = MimeType::IMAGE_JPEG;
|
|
$hash = Hash::create($content, HashAlgorithm::SHA256);
|
|
$meta = AssetMetadata::empty();
|
|
$tags = ['hero', 'landing-page'];
|
|
|
|
$this->deduplicationService->shouldReceive('checkDuplicate')
|
|
->once()
|
|
->with($hash)
|
|
->andReturn(null);
|
|
|
|
$this->metadataExtractor->shouldReceive('extractImageMetadata')
|
|
->once()
|
|
->andReturn($meta);
|
|
|
|
$this->storage->shouldReceive('put')
|
|
->once()
|
|
->andReturnNull();
|
|
|
|
$this->assetRepository->shouldReceive('save')
|
|
->once()
|
|
->andReturnNull();
|
|
|
|
$this->tagRepository->shouldReceive('addTags')
|
|
->once()
|
|
->andReturnNull();
|
|
|
|
$this->commandBus->shouldReceive('dispatch')
|
|
->once()
|
|
->andReturnNull();
|
|
|
|
$asset = $this->service->upload($content, $bucket, $mime, null, $tags);
|
|
|
|
expect($asset)->toBeInstanceOf(Asset::class);
|
|
});
|
|
|
|
it('finds asset by id', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, $assetId);
|
|
|
|
$this->assetRepository->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$found = $this->service->findById($assetId);
|
|
|
|
expect($found)->toBe($asset);
|
|
});
|
|
|
|
it('throws exception when asset not found', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
|
|
$this->assetRepository->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn(null);
|
|
|
|
expect(fn () => $this->service->findById($assetId))
|
|
->toThrow(AssetNotFoundException::class);
|
|
});
|
|
|
|
it('gets variants for asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$variants = [
|
|
AssetTestHelpers::createAssetVariant($assetId),
|
|
];
|
|
|
|
$this->variantRepository->shouldReceive('findByAsset')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($variants);
|
|
|
|
$found = $this->service->getVariants($assetId);
|
|
|
|
expect($found)->toBe($variants);
|
|
});
|
|
|
|
it('gets specific variant', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$variantName = VariantName::fromString('1200w.webp');
|
|
$variant = AssetTestHelpers::createAssetVariant($assetId, $variantName);
|
|
|
|
$this->variantRepository->shouldReceive('findByAssetAndVariant')
|
|
->once()
|
|
->with($assetId, $variantName)
|
|
->andReturn($variant);
|
|
|
|
$found = $this->service->getVariant($assetId, $variantName);
|
|
|
|
expect($found)->toBe($variant);
|
|
});
|
|
|
|
it('finds assets by tag', function () {
|
|
$tag = 'hero';
|
|
$asset = AssetTestHelpers::createAsset($this->clock);
|
|
$assetTag = new AssetTag($asset->id, $tag);
|
|
|
|
$this->tagRepository->shouldReceive('findByTag')
|
|
->once()
|
|
->with($tag)
|
|
->andReturn([$assetTag]);
|
|
|
|
$this->assetRepository->shouldReceive('findById')
|
|
->once()
|
|
->with($asset->id)
|
|
->andReturn($asset);
|
|
|
|
$found = $this->service->findByTag($tag);
|
|
|
|
expect($found)->toHaveCount(1);
|
|
expect($found[0])->toBe($asset);
|
|
});
|
|
|
|
it('finds assets by bucket', function () {
|
|
$bucket = BucketName::fromString('media');
|
|
$assets = [
|
|
AssetTestHelpers::createAsset($this->clock),
|
|
];
|
|
|
|
$this->assetRepository->shouldReceive('findByBucket')
|
|
->once()
|
|
->with($bucket)
|
|
->andReturn($assets);
|
|
|
|
$found = $this->service->findByBucket($bucket);
|
|
|
|
expect($found)->toBe($assets);
|
|
});
|
|
|
|
it('deletes asset and all variants', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, $assetId);
|
|
$variant = AssetTestHelpers::createAssetVariant($assetId);
|
|
|
|
$this->assetRepository->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->storage->shouldReceive('delete')
|
|
->twice()
|
|
->andReturnNull();
|
|
|
|
$this->variantRepository->shouldReceive('findByAsset')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn([$variant]);
|
|
|
|
$this->variantRepository->shouldReceive('deleteByAsset')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturnNull();
|
|
|
|
$this->tagRepository->shouldReceive('deleteByAsset')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturnNull();
|
|
|
|
$this->assetRepository->shouldReceive('delete')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturnNull();
|
|
|
|
$this->service->delete($assetId);
|
|
});
|
|
|
|
it('adds tags to asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$tags = ['hero', 'landing-page'];
|
|
|
|
$this->tagRepository->shouldReceive('addTags')
|
|
->once()
|
|
->with($assetId, $tags)
|
|
->andReturnNull();
|
|
|
|
$this->service->addTags($assetId, $tags);
|
|
});
|
|
|
|
it('removes tags from asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$tags = ['hero'];
|
|
|
|
$this->tagRepository->shouldReceive('removeTags')
|
|
->once()
|
|
->with($assetId, $tags)
|
|
->andReturnNull();
|
|
|
|
$this->service->removeTags($assetId, $tags);
|
|
});
|
|
|
|
it('gets tags for asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$tags = [
|
|
new AssetTag($assetId, 'hero'),
|
|
new AssetTag($assetId, 'landing-page'),
|
|
];
|
|
|
|
$this->tagRepository->shouldReceive('getTags')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($tags);
|
|
|
|
$found = $this->service->getTags($assetId);
|
|
|
|
expect($found)->toBe($tags);
|
|
});
|
|
});
|
|
|