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

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc