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); }); });