image_to_media_swapper-2.x-dev/tests/src/Kernel/SwapperServiceTest.php
tests/src/Kernel/SwapperServiceTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\image_to_media_swapper\Kernel;
use Drupal\Core\File\FileSystemInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\file\Entity\File;
use Drupal\image_to_media_swapper\SwapperService;
use Drupal\KernelTests\KernelTestBase;
use Drupal\media\Entity\Media;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
/**
* Tests the SwapperService for various file types and scenarios.
*
* @group image_to_media_swapper
* @coversDefaultClass \Drupal\image_to_media_swapper\SwapperService
*/
class SwapperServiceTest extends KernelTestBase {
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'image_to_media_swapper',
'system',
'field',
'file',
'media',
'image',
'user',
'options',
'serialization',
];
/**
* The swapper service.
*/
protected SwapperService $swapperService;
/**
* The file system service.
*/
protected FileSystemInterface $fileSystem;
/**
* Test files directory.
*/
protected string $testFilesDir;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installEntitySchema('media');
$this->installSchema('file', ['file_usage']);
$this->installConfig(['media', 'file', 'system']);
// Setup basic config for the service without full schema validation.
$config = $this->container->get('config.factory')->getEditable('image_to_media_swapper.security_settings');
$config->setData([
'enable_remote_downloads' => TRUE,
'max_file_size' => 10,
'download_timeout' => 30,
'restrict_domains' => FALSE,
'allowed_domains' => [],
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'],
'block_private_ips' => FALSE,
'require_https' => FALSE,
'max_redirects' => 3,
])->save();
// Create and use the test-specific swapper service that handles VFS paths.
$this->swapperService = new TestSwapperService(
$this->container->get('entity_type.manager'),
$this->container->get('http_client'),
$this->container->get('file_system'),
$this->container->get('logger.factory'),
$this->container->get('image_to_media_swapper.security_validation'),
$this->container->get('config.factory'),
$this->container->get('entity_type.bundle.info'),
$this->container->get('entity_field.manager'),
$this->container->get('module_handler'),
$this->container->get('file.mime_type.guesser'),
$this->container->get('image_to_media_swapper.content_verification')
);
$this->fileSystem = $this->container->get('file_system');
// Setup test files directory.
$this->testFilesDir = $this->fileSystem->getTempDirectory() . '/swapper_test';
$this->fileSystem->prepareDirectory($this->testFilesDir, FileSystemInterface::CREATE_DIRECTORY);
// Create media types with file fields for testing.
$this->createMediaType('image', [
'id' => 'image',
'name' => 'Image',
'source_configuration' => [
'source_field' => 'field_media_image',
],
]);
$this->createMediaType('file', [
'id' => 'document',
'name' => 'Document',
'source_configuration' => [
'source_field' => 'field_media_document',
],
]);
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
// Configure file extensions for the automatically created fields.
$imageField = FieldConfig::loadByName('media', 'image', 'field_media_image');
if ($imageField) {
$imageField->setSetting('file_extensions', 'jpg jpeg png webp gif');
$imageField->setSetting('alt_field', TRUE);
$imageField->setSetting('title_field', TRUE);
$imageField->save();
}
$documentField = FieldConfig::loadByName('media', 'document', 'field_media_document');
if ($documentField) {
$documentField->setSetting('file_extensions', 'pdf txt doc docx');
$documentField->setSetting('description_field', TRUE);
$documentField->save();
}
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
// Clean up test files.
if (is_dir($this->testFilesDir)) {
$this->cleanupTestFiles($this->testFilesDir);
}
parent::tearDown();
}
/**
* Tests image file processing (local).
*
* @covers ::findOrCreateFileEntityByUri
* @covers ::findOrCreateMediaFromFileEntity
* @covers ::getMediaBundleForMimeType
* @covers ::getFieldNameForBundle
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testLocalImageProcessing(): void {
// Create test image files.
$testCases = [
['filename' => 'test.jpg', 'content' => $this->createTestJpegContent(), 'mime' => 'image/jpeg'],
['filename' => 'test.png', 'content' => $this->createTestPngContent(), 'mime' => 'image/png'],
['filename' => 'test.gif', 'content' => $this->createTestGifContent(), 'mime' => 'image/gif'],
['filename' => 'test.webp', 'content' => $this->createTestWebpContent(), 'mime' => 'image/webp'],
];
foreach ($testCases as $testCase) {
$filePath = $this->testFilesDir . '/' . $testCase['filename'];
file_put_contents($filePath, $testCase['content']);
// Create public:// URI.
$publicDir = $this->fileSystem->realpath('public://');
$this->fileSystem->prepareDirectory($publicDir, FileSystemInterface::CREATE_DIRECTORY);
$publicPath = $publicDir . '/' . $testCase['filename'];
copy($filePath, $publicPath);
$publicUri = 'public://' . $testCase['filename'];
// Test file entity creation.
$file = $this->swapperService->findOrCreateFileEntityByUri($publicUri);
$this->assertInstanceOf(File::class, $file, "File entity should be created for {$testCase['filename']}");
$this->assertEquals($testCase['mime'], $file->getMimeType(), "MIME type should match for {$testCase['filename']}");
$this->assertEquals($testCase['filename'], $file->getFilename(), "Filename should match for {$testCase['filename']}");
// Test media entity creation.
$media = $this->swapperService->findOrCreateMediaFromFileEntity($file);
$this->assertInstanceOf(Media::class, $media, "Media entity should be created for {$testCase['filename']}");
$this->assertEquals('image', $media->bundle(), "Media bundle should be 'image' for {$testCase['filename']}");
$this->assertEquals($testCase['filename'], strtolower($media->getName()), "Media name should match filename for {$testCase['filename']}");
// Test that subsequent calls return the same entities.
$file2 = $this->swapperService->findOrCreateFileEntityByUri($publicUri);
$this->assertEquals($file->id(), $file2->id(), "Should return existing file entity for {$testCase['filename']}");
$media2 = $this->swapperService->findOrCreateMediaFromFileEntity($file);
$this->assertEquals($media->id(), $media2->id(), "Should return existing media entity for {$testCase['filename']}");
}
}
/**
* Tests file processing (local).
*
* @covers ::validateAndProcessFilePath
* @covers ::findOrCreateFileEntityByUri
* @covers ::findOrCreateMediaFromFileEntity
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testLocalFileProcessing(): void {
// Create test file.
$fileContent = $this->createTestFileContent();
$filename = 'test.pdf';
$filePath = $this->testFilesDir . '/' . $filename;
file_put_contents($filePath, $fileContent);
// Copy to public directory.
$publicDir = $this->fileSystem->realpath('public://');
$this->fileSystem->prepareDirectory($publicDir, FileSystemInterface::CREATE_DIRECTORY);
$publicPath = $publicDir . '/' . $filename;
copy($filePath, $publicPath);
// Test file path validation and processing.
$webPath = '/sites/default/files/' . $filename;
$media = $this->swapperService->validateAndProcessFilePath($webPath);
$this->assertInstanceOf(Media::class, $media, "Media entity should be created for PDF");
$this->assertEquals('document', $media->bundle(), "Media bundle should be 'document' for PDF");
$this->assertEquals($filename, strtolower($media->getName()), "Media name should match filename for PDF");
// Test direct URI processing.
$publicUri = 'public://' . $filename;
$file = $this->swapperService->findOrCreateFileEntityByUri($publicUri);
$this->assertInstanceOf(File::class, $file, "File entity should be created for PDF");
$this->assertEquals('application/pdf', $file->getMimeType(), "MIME type should be application/pdf");
}
/**
* Tests remote image file processing.
*
* @covers ::validateAndProcessRemoteFile
* @covers ::downloadRemoteFile
*/
public function testRemoteImageProcessing(): void {
// Mock HTTP client for remote downloads.
$testCases = [
[
'url' => 'https://example.com/test.jpg',
'content' => $this->createTestJpegContent(),
'contentType' => 'image/jpeg',
'filename' => 'test.jpg',
],
[
'url' => 'https://example.com/test.png',
'content' => $this->createTestPngContent(),
'contentType' => 'image/png',
'filename' => 'test.png',
],
];
foreach ($testCases as $testCase) {
$mockHandler = new MockHandler([
new Response(200, [
'Content-Type' => $testCase['contentType'],
'Content-Length' => strlen($testCase['content']),
]),
new Response(200, [], $testCase['content']),
]);
$handlerStack = HandlerStack::create($mockHandler);
$mockClient = new Client(['handler' => $handlerStack]);
// Replace the HTTP client in the service.
$this->replaceMockHttpClient($mockClient);
// Enable remote downloads for testing.
$config = $this->config('image_to_media_swapper.security_settings');
$config->set('enable_remote_downloads', TRUE);
$config->set('max_file_size', 10);
$config->save();
// Test remote download.
$publicUri = 'public://' . $testCase['filename'];
$result = $this->swapperService->downloadRemoteFile($testCase['url'], $publicUri);
$this->assertEquals($publicUri, $result, "Remote download should succeed for {$testCase['url']}");
$this->assertFileExists($this->fileSystem->realpath($publicUri), "Downloaded file should exist for {$testCase['url']}");
// Verify file content.
$downloadedContent = file_get_contents($this->fileSystem->realpath($publicUri));
$this->assertEquals($testCase['content'], $downloadedContent, "Downloaded content should match for {$testCase['url']}");
}
}
/**
* Tests remote file processing.
*
* @covers ::validateAndProcessRemoteFile
* @covers ::downloadRemoteFile
* @covers ::generateSafeFileName
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testRemoteFileProcessing(): void {
$fileContent = $this->createTestFileContent();
$fileUrl = 'https://example.com/document.pdf';
// Mock HTTP client.
$mockHandler = new MockHandler([
new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Length' => strlen($fileContent),
]),
new Response(200, [], $fileContent),
]);
$handlerStack = HandlerStack::create($mockHandler);
$mockClient = new Client(['handler' => $handlerStack]);
$this->replaceMockHttpClient($mockClient);
// Enable remote downloads.
$config = $this->config('image_to_media_swapper.security_settings');
$config->set('enable_remote_downloads', TRUE);
$config->set('max_file_size', 10);
$config->save();
// Test remote file processing.
$result = $this->swapperService->validateAndProcessRemoteFile($fileUrl);
$this->assertInstanceOf(Media::class, $result, "Remote PDF should create media entity");
$this->assertEquals('document', $result->bundle(), "Media bundle should be 'document' for remote PDF");
$this->assertStringContainsString('Document', $result->getName(), "Media name should contain 'document' for remote PDF");
}
/**
* Tests error handling for invalid files.
*
* @covers ::validateAndProcessFilePath
* @covers ::validateAndProcessRemoteFile
* @covers ::isSupportedFile
* @covers ::isSupportedFileUrl
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function dontTestInvalidFileHandling(): void {
// Test invalid PDF path.
$invalidPath = '/nonexistent/file.pdf';
$result = $this->swapperService->validateAndProcessFilePath($invalidPath);
$this->assertNull($result, "Invalid PDF path should return null");
// Test non-PDF file.
$txtContent = 'This is not a PDF file';
$filename = 'fake.pdf';
$filePath = $this->testFilesDir . '/' . $filename;
file_put_contents($filePath, $txtContent);
$publicDir = $this->fileSystem->realpath('public://');
$publicPath = $publicDir . '/' . $filename;
copy($filePath, $publicPath);
$webPath = '/sites/default/files/' . $filename;
$result = $this->swapperService->validateAndProcessFilePath($webPath);
$this->assertNull($result, "Non-PDF file should return null");
// Test invalid remote file URL.
$invalidUrl = 'https://example.com/document.txt';
$result = $this->swapperService->validateAndProcessRemoteFile($invalidUrl);
$this->assertIsString($result, "Invalid URL should return error message");
$this->assertStringContainsString('does not appear to be a valid file type', $result);
}
/**
* Tests file path conversion methods.
*
* @covers ::convertWebPathToPublicUri
*/
public function testWebPathToPublicUriConversion(): void {
$testCases = [
// Standard web paths.
['/sites/default/files/test.jpg', 'public://test.jpg'],
['/sites/default/files/subfolder/test.pdf', 'public://subfolder/test.pdf'],
// URL inputs.
['https://example.com/sites/default/files/test.jpg', 'public://test.jpg'],
['http://localhost/sites/default/files/doc.pdf', 'public://doc.pdf'],
// Just filenames.
['test.jpg', 'public://test.jpg'],
['document.pdf', 'public://document.pdf'],
];
foreach ($testCases as [$input, $expected]) {
$result = $this->swapperService->convertWebPathToPublicUri($input);
$this->assertEquals($expected, $result, "Web path conversion should work for: {$input}");
}
}
/**
* Tests bundle and field detection.
*
* @covers ::getMediaBundlesWithFileFields
* @covers ::getAvailableExtensions
*/
public function testBundleAndFieldDetection(): void {
$bundles = $this->swapperService->getMediaBundlesWithFileFields(TRUE);
$this->assertIsArray($bundles, "Should return array of bundles");
$this->assertArrayHasKey('image', $bundles, "Should detect image bundle");
$this->assertArrayHasKey('document', $bundles, "Should detect document bundle");
// Test available extensions.
$extensions = $this->swapperService->getAvailableExtensions();
$this->assertIsArray($extensions, "Should return array of extensions");
$this->assertContains('jpg', $extensions, "Should include jpg extension");
$this->assertContains('pdf', $extensions, "Should include pdf extension");
}
/**
* Tests UUID-based file processing.
*
* @covers ::findFileFromUuid
* @covers ::validateAndProcessFileUuid
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testUuidBasedProcessing(): void {
// Create a test file and file entity.
$fileContent = $this->createTestFileContent();
$filename = 'uuid-test.pdf';
$publicUri = 'public://' . $filename;
$publicDir = $this->fileSystem->realpath('public://');
$publicPath = $publicDir . '/' . $filename;
file_put_contents($publicPath, $fileContent);
$file = File::create([
'uri' => $publicUri,
'filename' => $filename,
'filemime' => 'application/pdf',
'status' => 1,
]);
$file->save();
$uuid = $file->uuid();
// Test UUID-based file finding.
$foundFile = $this->swapperService->findFileFromUuid($uuid);
$this->assertInstanceOf(File::class, $foundFile, "Should find file by UUID");
$this->assertEquals($file->id(), $foundFile->id(), "Should return correct file entity");
// Test UUID-based file processing.
$media = $this->swapperService->validateAndProcessFileUuid($uuid);
$this->assertInstanceOf(Media::class, $media, "Should create media from PDF UUID");
$this->assertEquals('document', $media->bundle(), "Should use document bundle for PDF");
// Test with invalid UUID.
$invalidUuid = 'invalid-uuid-string';
$result = $this->swapperService->findFileFromUuid($invalidUuid);
$this->assertNull($result, "Should return null for invalid UUID");
$result = $this->swapperService->validateAndProcessFileUuid($invalidUuid);
$this->assertNull($result, "Should return null for invalid PDF UUID");
}
/**
* Creates test JPEG content with valid header.
*/
protected function createTestJpegContent(): string {
// Minimal valid JPEG file.
return "\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xFF\xDB\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0C\x14\r\x0C\x0B\x0B\x0C\x19\x12\x13\x0F\x14\x1D\x1A\x1F\x1E\x1D\x1A\x1C\x1C $.' \",#\x1C\x1C(7),01444\x1F'9=82<.342\xFF\xC0\x00\x11\x08\x00\x01\x00\x01\x01\x01\x11\x00\x02\x11\x01\x03\x11\x01\xFF\xC4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xFF\xDA\x00\x08\x01\x01\x00\x00?\x00\x55\xFF\xD9";
}
/**
* Creates test PNG content with valid header.
*/
protected function createTestPngContent(): string {
// Minimal valid PNG file.
return "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xDE\x00\x00\x00\tpHYs\x00\x00\x0B\x13\x00\x00\x0B\x13\x01\x00\x9A\x9C\x18\x00\x00\x00\x0BIDAT\x08\x1Dc\xF8\x00\x00\x00\x01\x00\x01\x02\xB4\x80\x8D\x00\x00\x00\x00IEND\xAEB`\x82";
}
/**
* Creates test GIF content with valid header.
*/
protected function createTestGifContent(): string {
// Minimal valid GIF file.
return "GIF89a\x01\x00\x01\x00\x00\x00\x00!\xF9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x04\x01\x00;";
}
/**
* Creates test WebP content with valid header.
*/
protected function createTestWebpContent(): string {
// Minimal valid WebP file.
return "RIFF\x1A\x00\x00\x00WEBPVP8 \x0E\x00\x00\x00\x30\x01\x00\x9D\x01*\x01\x00\x01\x00\x01\x00\x14\x00\x00\x00";
}
/**
* Creates test PDF content with valid header and structure.
*/
protected function createTestFileContent(): string {
return "%PDF-1.4\n%âÏÓ\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\nxref\n0 4\n0000000000 65535 f \n0000000015 00000 n \n0000000068 00000 n \n0000000125 00000 n \ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n204\n%%EOF";
}
/**
* Replaces the HTTP client in the SwapperService with a mock.
*/
protected function replaceMockHttpClient(Client $mockClient): void {
// Get the service container.
$container = $this->container;
// Replace the HTTP client service.
$container->set('http_client', $mockClient);
// Recreate the SwapperService with test-specific version.
$this->swapperService = new TestSwapperService(
$container->get('entity_type.manager'),
$mockClient,
$container->get('file_system'),
$container->get('logger.factory'),
$container->get('image_to_media_swapper.security_validation'),
$container->get('config.factory'),
$container->get('entity_type.bundle.info'),
$container->get('entity_field.manager'),
$container->get('module_handler'),
$container->get('file.mime_type.guesser'),
$container->get('image_to_media_swapper.content_verification'),
);
}
/**
* Recursively clean up test files and directories.
*/
protected function cleanupTestFiles(string $directory): void {
if (!is_dir($directory)) {
return;
}
$files = array_diff(scandir($directory), ['.', '..']);
foreach ($files as $file) {
$path = $directory . '/' . $file;
if (is_dir($path)) {
$this->cleanupTestFiles($path);
rmdir($path);
}
else {
unlink($path);
}
}
rmdir($directory);
}
}
