- 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)
302 lines
9.4 KiB
PHP
302 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Application\Asset\Api\V1\AssetsController;
|
|
use App\Domain\Asset\Entities\Asset;
|
|
use App\Domain\Asset\Entities\AssetTag;
|
|
use App\Domain\Asset\Entities\AssetVariant;
|
|
use App\Domain\Asset\Services\AssetService;
|
|
use App\Domain\Asset\Storage\AssetStorageInterface;
|
|
use App\Domain\Asset\ValueObjects\AssetId;
|
|
use App\Domain\Asset\ValueObjects\VariantName;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Http\HttpRequest;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Http\RequestId;
|
|
use App\Framework\Http\Status;
|
|
use App\Framework\Serializer\Json\JsonSerializer;
|
|
|
|
describe('AssetsController', function () {
|
|
beforeEach(function () {
|
|
$this->clock = new SystemClock();
|
|
$this->assetService = Mockery::mock(AssetService::class);
|
|
$this->storage = Mockery::mock(AssetStorageInterface::class);
|
|
$this->jsonSerializer = new JsonSerializer();
|
|
$this->controller = new AssetsController(
|
|
$this->assetService,
|
|
$this->storage,
|
|
$this->jsonSerializer
|
|
);
|
|
});
|
|
|
|
it('uploads asset successfully', function () {
|
|
$asset = AssetTestHelpers::createAsset($this->clock);
|
|
|
|
$this->assetService->shouldReceive('upload')
|
|
->once()
|
|
->andReturn($asset);
|
|
|
|
$request = new HttpRequest(
|
|
method: Method::POST,
|
|
path: '/api/v1/assets/upload',
|
|
files: [
|
|
'file' => [
|
|
'tmp_name' => '/tmp/test.jpg',
|
|
'type' => 'image/jpeg',
|
|
'name' => 'test.jpg',
|
|
],
|
|
],
|
|
id: new RequestId('test')
|
|
);
|
|
|
|
// Mock is_uploaded_file
|
|
$uploadRequest = new \App\Application\Asset\Api\Requests\UploadAssetRequest();
|
|
$response = $this->controller->upload($request, $uploadRequest);
|
|
|
|
expect($response->status)->toBe(Status::CREATED);
|
|
});
|
|
|
|
it('returns 400 when no file provided', function () {
|
|
$request = new HttpRequest(
|
|
method: Method::POST,
|
|
path: '/api/v1/assets/upload',
|
|
files: [],
|
|
id: new RequestId('test')
|
|
);
|
|
|
|
$uploadRequest = new \App\Application\Asset\Api\Requests\UploadAssetRequest();
|
|
$response = $this->controller->upload($request, $uploadRequest);
|
|
|
|
expect($response->status)->toBe(Status::BAD_REQUEST);
|
|
});
|
|
|
|
it('shows asset by id', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, id: $assetId);
|
|
$variants = [];
|
|
$tags = [];
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->assetService->shouldReceive('getVariants')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($variants);
|
|
|
|
$this->assetService->shouldReceive('getTags')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($tags);
|
|
|
|
$this->storage->shouldReceive('getUrl')
|
|
->once()
|
|
->andReturn('https://cdn.example.com/media/orig/test.jpg');
|
|
|
|
$response = $this->controller->show($assetId->toString());
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
|
|
it('returns 404 when asset not found', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->andThrow(\App\Domain\Asset\Exceptions\AssetNotFoundException::forId($assetId));
|
|
|
|
$response = $this->controller->show($assetId->toString());
|
|
|
|
expect($response->status)->toBe(Status::NOT_FOUND);
|
|
});
|
|
|
|
it('lists variants for asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, id: $assetId);
|
|
$variants = [
|
|
AssetTestHelpers::createAssetVariant($assetId),
|
|
];
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->assetService->shouldReceive('getVariants')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($variants);
|
|
|
|
$this->storage->shouldReceive('getUrl')
|
|
->once()
|
|
->andReturn('https://cdn.example.com/variants/test.jpg');
|
|
|
|
$response = $this->controller->variants($assetId->toString());
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
|
|
it('shows specific variant', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, id: $assetId);
|
|
$variantName = VariantName::fromString('1200w.webp');
|
|
$variant = AssetTestHelpers::createAssetVariant($assetId, $variantName);
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->assetService->shouldReceive('getVariant')
|
|
->once()
|
|
->with($assetId, $variantName)
|
|
->andReturn($variant);
|
|
|
|
$this->storage->shouldReceive('getUrl')
|
|
->once()
|
|
->andReturn('https://cdn.example.com/variants/test/1200w.webp');
|
|
|
|
$response = $this->controller->variant($assetId->toString(), '1200w.webp');
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
|
|
it('returns 404 when variant not found', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, id: $assetId);
|
|
$variantName = VariantName::fromString('1200w.webp');
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->assetService->shouldReceive('getVariant')
|
|
->once()
|
|
->with($assetId, $variantName)
|
|
->andReturn(null);
|
|
|
|
$response = $this->controller->variant($assetId->toString(), '1200w.webp');
|
|
|
|
expect($response->status)->toBe(Status::NOT_FOUND);
|
|
});
|
|
|
|
it('gets asset URL', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, id: $assetId);
|
|
$url = 'https://cdn.example.com/media/orig/test.jpg';
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->storage->shouldReceive('getUrl')
|
|
->once()
|
|
->with($asset)
|
|
->andReturn($url);
|
|
|
|
$response = $this->controller->url($assetId->toString());
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
|
|
it('gets signed URL for asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
$asset = AssetTestHelpers::createAsset($this->clock, id: $assetId);
|
|
$signedUrl = 'https://cdn.example.com/media/orig/test.jpg?signature=abc123';
|
|
|
|
$this->assetService->shouldReceive('findById')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturn($asset);
|
|
|
|
$this->storage->shouldReceive('getSignedUrl')
|
|
->once()
|
|
->with($asset, 3600)
|
|
->andReturn($signedUrl);
|
|
|
|
$request = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/api/v1/assets/{id}/signed-url',
|
|
query: ['expires' => '3600'],
|
|
id: new RequestId('test')
|
|
);
|
|
|
|
$response = $this->controller->signedUrl($request, $assetId->toString());
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
|
|
it('deletes asset', function () {
|
|
$assetId = AssetId::generate($this->clock);
|
|
|
|
$this->assetService->shouldReceive('delete')
|
|
->once()
|
|
->with($assetId)
|
|
->andReturnNull();
|
|
|
|
$response = $this->controller->delete($assetId->toString());
|
|
|
|
expect($response->status)->toBe(Status::NO_CONTENT);
|
|
});
|
|
|
|
it('lists assets by tag', function () {
|
|
$tag = 'hero';
|
|
$assets = [
|
|
AssetTestHelpers::createAsset($this->clock),
|
|
];
|
|
|
|
$this->assetService->shouldReceive('findByTag')
|
|
->once()
|
|
->with($tag)
|
|
->andReturn($assets);
|
|
|
|
$this->storage->shouldReceive('getUrl')
|
|
->once()
|
|
->andReturn('https://cdn.example.com/media/orig/test.jpg');
|
|
|
|
$request = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/api/v1/assets',
|
|
query: ['tag' => $tag],
|
|
id: new RequestId('test')
|
|
);
|
|
|
|
$response = $this->controller->index($request);
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
|
|
it('lists assets by bucket', function () {
|
|
$bucket = 'media';
|
|
$assets = [
|
|
AssetTestHelpers::createAsset($this->clock),
|
|
];
|
|
|
|
$this->assetService->shouldReceive('findByBucket')
|
|
->once()
|
|
->with(Mockery::type(\App\Framework\Storage\ValueObjects\BucketName::class))
|
|
->andReturn($assets);
|
|
|
|
$this->storage->shouldReceive('getUrl')
|
|
->once()
|
|
->andReturn('https://cdn.example.com/media/orig/test.jpg');
|
|
|
|
$request = new HttpRequest(
|
|
method: Method::GET,
|
|
path: '/api/v1/assets',
|
|
query: ['bucket' => $bucket],
|
|
id: new RequestId('test')
|
|
);
|
|
|
|
$response = $this->controller->index($request);
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
});
|
|
|