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:
47
tests/Unit/Domain/Asset/ValueObjects/AssetIdTest.php
Normal file
47
tests/Unit/Domain/Asset/ValueObjects/AssetIdTest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Asset\ValueObjects\AssetId;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
|
||||
describe('AssetId', function () {
|
||||
it('can be created from valid ULID string', function () {
|
||||
$validUlid = '01ARZ3NDEKTSV4RRFFQ69G5FAV';
|
||||
$assetId = AssetId::fromString($validUlid);
|
||||
|
||||
expect($assetId->toString())->toBe($validUlid);
|
||||
expect((string) $assetId)->toBe($validUlid);
|
||||
});
|
||||
|
||||
it('throws exception for invalid ULID format', function () {
|
||||
expect(fn () => AssetId::fromString('invalid-id'))
|
||||
->toThrow(InvalidArgumentException::class, 'Invalid Asset ID format');
|
||||
});
|
||||
|
||||
it('can generate new AssetId with Clock', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
|
||||
expect($assetId)->toBeInstanceOf(AssetId::class);
|
||||
expect($assetId->toString())->toMatch('/^[0-9A-Z]{26}$/');
|
||||
});
|
||||
|
||||
it('generates unique IDs', function () {
|
||||
$clock = new SystemClock();
|
||||
$id1 = AssetId::generate($clock);
|
||||
$id2 = AssetId::generate($clock);
|
||||
|
||||
expect($id1->toString())->not->toBe($id2->toString());
|
||||
});
|
||||
|
||||
it('can compare two AssetIds for equality', function () {
|
||||
$id1 = AssetId::fromString('01ARZ3NDEKTSV4RRFFQ69G5FAV');
|
||||
$id2 = AssetId::fromString('01ARZ3NDEKTSV4RRFFQ69G5FAV');
|
||||
$id3 = AssetId::fromString('01ARZ3NDEKTSV4RRFFQ69G5FAW');
|
||||
|
||||
expect($id1->equals($id2))->toBeTrue();
|
||||
expect($id1->equals($id3))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
158
tests/Unit/Domain/Asset/ValueObjects/AssetMetadataTest.php
Normal file
158
tests/Unit/Domain/Asset/ValueObjects/AssetMetadataTest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Asset\ValueObjects\AssetMetadata;
|
||||
|
||||
describe('AssetMetadata', function () {
|
||||
it('can be created as empty', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
|
||||
expect($meta->toArray())->toBe([]);
|
||||
});
|
||||
|
||||
it('can be created from array', function () {
|
||||
$meta = AssetMetadata::fromArray([
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
]);
|
||||
|
||||
expect($meta->getWidth())->toBe(1920);
|
||||
expect($meta->getHeight())->toBe(1080);
|
||||
});
|
||||
|
||||
it('can get width and height', function () {
|
||||
$meta = AssetMetadata::fromArray([
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
]);
|
||||
|
||||
expect($meta->getWidth())->toBe(1920);
|
||||
expect($meta->getHeight())->toBe(1080);
|
||||
});
|
||||
|
||||
it('returns null for missing dimensions', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
|
||||
expect($meta->getWidth())->toBeNull();
|
||||
expect($meta->getHeight())->toBeNull();
|
||||
});
|
||||
|
||||
it('can set width immutably', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withWidth(1920);
|
||||
|
||||
expect($newMeta->getWidth())->toBe(1920);
|
||||
expect($meta->getWidth())->toBeNull(); // Original unchanged
|
||||
});
|
||||
|
||||
it('can set height immutably', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withHeight(1080);
|
||||
|
||||
expect($newMeta->getHeight())->toBe(1080);
|
||||
expect($meta->getHeight())->toBeNull(); // Original unchanged
|
||||
});
|
||||
|
||||
it('can set dimensions immutably', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withDimensions(1920, 1080);
|
||||
|
||||
expect($newMeta->getWidth())->toBe(1920);
|
||||
expect($newMeta->getHeight())->toBe(1080);
|
||||
expect($meta->getWidth())->toBeNull(); // Original unchanged
|
||||
});
|
||||
|
||||
it('can set duration', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withDuration(120);
|
||||
|
||||
expect($newMeta->getDuration())->toBe(120);
|
||||
});
|
||||
|
||||
it('can set EXIF data', function () {
|
||||
$exif = ['ISO' => 400, 'Aperture' => 'f/2.8'];
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withExif($exif);
|
||||
|
||||
expect($newMeta->getExif())->toBe($exif);
|
||||
});
|
||||
|
||||
it('can set IPTC data', function () {
|
||||
$iptc = ['Copyright' => '2025'];
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withIptc($iptc);
|
||||
|
||||
expect($newMeta->getIptc())->toBe($iptc);
|
||||
});
|
||||
|
||||
it('can set color profile', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withColorProfile('sRGB');
|
||||
|
||||
expect($newMeta->getColorProfile())->toBe('sRGB');
|
||||
});
|
||||
|
||||
it('can get color profile with snake_case key', function () {
|
||||
$meta = AssetMetadata::fromArray(['color_profile' => 'sRGB']);
|
||||
|
||||
expect($meta->getColorProfile())->toBe('sRGB');
|
||||
});
|
||||
|
||||
it('can set focal point', function () {
|
||||
$focalPoint = ['x' => 0.5, 'y' => 0.5];
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withFocalPoint($focalPoint);
|
||||
|
||||
expect($newMeta->getFocalPoint())->toBe($focalPoint);
|
||||
});
|
||||
|
||||
it('can get focal point with snake_case key', function () {
|
||||
$meta = AssetMetadata::fromArray(['focal_point' => ['x' => 0.5, 'y' => 0.5]]);
|
||||
|
||||
expect($meta->getFocalPoint())->not->toBeNull();
|
||||
});
|
||||
|
||||
it('can set dominant color', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withDominantColor('#FF5733');
|
||||
|
||||
expect($newMeta->getDominantColor())->toBe('#FF5733');
|
||||
});
|
||||
|
||||
it('can get dominant color with snake_case key', function () {
|
||||
$meta = AssetMetadata::fromArray(['dominant_color' => '#FF5733']);
|
||||
|
||||
expect($meta->getDominantColor())->toBe('#FF5733');
|
||||
});
|
||||
|
||||
it('can set blurhash', function () {
|
||||
$meta = AssetMetadata::empty();
|
||||
$newMeta = $meta->withBlurhash('LGF5]+Yk^6#M@-5c,1J5@[or[Q6.');
|
||||
|
||||
expect($newMeta->getBlurhash())->toBe('LGF5]+Yk^6#M@-5c,1J5@[or[Q6.');
|
||||
});
|
||||
|
||||
it('can get and set arbitrary values', function () {
|
||||
$meta = AssetMetadata::fromArray(['custom' => 'value']);
|
||||
|
||||
expect($meta->get('custom'))->toBe('value');
|
||||
expect($meta->has('custom'))->toBeTrue();
|
||||
expect($meta->has('missing'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('can convert to array', function () {
|
||||
$data = ['width' => 1920, 'height' => 1080];
|
||||
$meta = AssetMetadata::fromArray($data);
|
||||
|
||||
expect($meta->toArray())->toBe($data);
|
||||
});
|
||||
|
||||
it('can remove width by setting to null', function () {
|
||||
$meta = AssetMetadata::fromArray(['width' => 1920]);
|
||||
$newMeta = $meta->withWidth(null);
|
||||
|
||||
expect($newMeta->getWidth())->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Asset\ValueObjects\AssetId;
|
||||
use App\Domain\Asset\ValueObjects\ObjectKeyGenerator;
|
||||
use App\Domain\Asset\ValueObjects\VariantName;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\Storage\ValueObjects\ObjectKey;
|
||||
|
||||
describe('ObjectKeyGenerator', function () {
|
||||
it('generates key for original asset', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
$key = ObjectKeyGenerator::generateKey($assetId, 'jpg');
|
||||
|
||||
expect($key)->toBeInstanceOf(ObjectKey::class);
|
||||
expect($key->toString())->toContain('orig');
|
||||
expect($key->toString())->toContain($now->format('Y'));
|
||||
expect($key->toString())->toContain($now->format('m'));
|
||||
expect($key->toString())->toContain($now->format('d'));
|
||||
expect($key->toString())->toContain($assetId->toString());
|
||||
expect($key->toString())->toEndWith('.jpg');
|
||||
});
|
||||
|
||||
it('generates key with custom prefix', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
|
||||
$key = ObjectKeyGenerator::generateKey($assetId, 'png', 'custom');
|
||||
|
||||
expect($key->toString())->toStartWith('custom/');
|
||||
});
|
||||
|
||||
it('removes leading dot from extension', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
|
||||
$key1 = ObjectKeyGenerator::generateKey($assetId, '.jpg');
|
||||
$key2 = ObjectKeyGenerator::generateKey($assetId, 'jpg');
|
||||
|
||||
expect($key1->toString())->toBe($key2->toString());
|
||||
});
|
||||
|
||||
it('generates variant key', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
$variant = VariantName::fromString('1200w.webp');
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
$key = ObjectKeyGenerator::generateVariantKey($assetId, $variant);
|
||||
|
||||
expect($key)->toBeInstanceOf(ObjectKey::class);
|
||||
expect($key->toString())->toContain('variants');
|
||||
expect($key->toString())->toContain($now->format('Y'));
|
||||
expect($key->toString())->toContain($now->format('m'));
|
||||
expect($key->toString())->toContain($now->format('d'));
|
||||
expect($key->toString())->toContain($assetId->toString());
|
||||
expect($key->toString())->toEndWith('/1200w.webp');
|
||||
});
|
||||
|
||||
it('generates variant key with custom prefix', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
$variant = VariantName::fromString('thumb@1x');
|
||||
|
||||
$key = ObjectKeyGenerator::generateVariantKey($assetId, $variant, 'custom-variants');
|
||||
|
||||
expect($key->toString())->toStartWith('custom-variants/');
|
||||
});
|
||||
|
||||
it('generates keys with date-based structure', function () {
|
||||
$clock = new SystemClock();
|
||||
$assetId = AssetId::generate($clock);
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
$key = ObjectKeyGenerator::generateKey($assetId, 'jpg');
|
||||
|
||||
$expectedPattern = sprintf(
|
||||
'/^orig\/%s\/%s\/%s\/%s\.jpg$/',
|
||||
$now->format('Y'),
|
||||
$now->format('m'),
|
||||
$now->format('d'),
|
||||
preg_quote($assetId->toString(), '/')
|
||||
);
|
||||
|
||||
expect($key->toString())->toMatch($expectedPattern);
|
||||
});
|
||||
});
|
||||
|
||||
81
tests/Unit/Domain/Asset/ValueObjects/VariantNameTest.php
Normal file
81
tests/Unit/Domain/Asset/ValueObjects/VariantNameTest.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Asset\ValueObjects\VariantName;
|
||||
|
||||
describe('VariantName', function () {
|
||||
it('can be created from valid string', function () {
|
||||
$variant = VariantName::fromString('1200w.webp');
|
||||
|
||||
expect($variant->toString())->toBe('1200w.webp');
|
||||
expect((string) $variant)->toBe('1200w.webp');
|
||||
});
|
||||
|
||||
it('accepts variant names with scale', function () {
|
||||
$variant = VariantName::fromString('thumb@1x');
|
||||
|
||||
expect($variant->toString())->toBe('thumb@1x');
|
||||
expect($variant->getScale())->toBe('1x');
|
||||
});
|
||||
|
||||
it('accepts variant names with scale and extension', function () {
|
||||
$variant = VariantName::fromString('thumb@2x.webp');
|
||||
|
||||
expect($variant->toString())->toBe('thumb@2x.webp');
|
||||
expect($variant->getScale())->toBe('2x');
|
||||
expect($variant->getExtension())->toBe('webp');
|
||||
});
|
||||
|
||||
it('accepts variant names without scale or extension', function () {
|
||||
$variant = VariantName::fromString('cover');
|
||||
|
||||
expect($variant->toString())->toBe('cover');
|
||||
expect($variant->getScale())->toBeNull();
|
||||
expect($variant->getExtension())->toBeNull();
|
||||
});
|
||||
|
||||
it('extracts extension correctly', function () {
|
||||
expect(VariantName::fromString('image.webp')->getExtension())->toBe('webp');
|
||||
expect(VariantName::fromString('image.jpg')->getExtension())->toBe('jpg');
|
||||
expect(VariantName::fromString('waveform.json')->getExtension())->toBe('json');
|
||||
});
|
||||
|
||||
it('extracts scale correctly', function () {
|
||||
expect(VariantName::fromString('thumb@1x')->getScale())->toBe('1x');
|
||||
expect(VariantName::fromString('thumb@2x')->getScale())->toBe('2x');
|
||||
expect(VariantName::fromString('cover@3x.webp')->getScale())->toBe('3x');
|
||||
});
|
||||
|
||||
it('throws exception for empty string', function () {
|
||||
expect(fn () => VariantName::fromString(''))
|
||||
->toThrow(InvalidArgumentException::class, 'Variant name cannot be empty');
|
||||
});
|
||||
|
||||
it('throws exception for invalid format', function () {
|
||||
expect(fn () => VariantName::fromString('invalid@format'))
|
||||
->toThrow(InvalidArgumentException::class, 'Invalid variant name format');
|
||||
|
||||
expect(fn () => VariantName::fromString('UPPERCASE'))
|
||||
->toThrow(InvalidArgumentException::class);
|
||||
|
||||
expect(fn () => VariantName::fromString('with spaces'))
|
||||
->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('throws exception for variant name exceeding 100 characters', function () {
|
||||
$longName = str_repeat('a', 101);
|
||||
expect(fn () => VariantName::fromString($longName))
|
||||
->toThrow(InvalidArgumentException::class, 'Variant name cannot exceed 100 characters');
|
||||
});
|
||||
|
||||
it('can compare two VariantNames for equality', function () {
|
||||
$variant1 = VariantName::fromString('1200w.webp');
|
||||
$variant2 = VariantName::fromString('1200w.webp');
|
||||
$variant3 = VariantName::fromString('800w.webp');
|
||||
|
||||
expect($variant1->equals($variant2))->toBeTrue();
|
||||
expect($variant1->equals($variant3))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user