image_to_media_swapper-2.x-dev/tests/src/Kernel/HttpClientMockingTest.php

tests/src/Kernel/HttpClientMockingTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\image_to_media_swapper\Kernel;

use Drupal\image_to_media_swapper\SwapperService;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Request;

/**
 * Tests HTTP client mocking for external file downloads and network scenarios.
 *
 * @group image_to_media_swapper
 * @group http_mocking
 */
class HttpClientMockingTest 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;

  /**
   * {@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 security config.
    $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 media types for testing.
    $this->createMediaType('image', [
      'id' => 'image',
      'source_configuration' => ['source_field' => 'field_media_image'],
    ]);
    $this->createMediaType('file', [
      'id' => 'document',
      'source_configuration' => ['source_field' => 'field_media_document'],
    ]);

    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
    $this->swapperService = $this->container->get('image_to_media_swapper.service');
  }

  /**
   * Tests successful remote image download with proper HTTP response.
   */
  public function testSuccessfulRemoteImageDownload(): void {
    $image_content = $this->createTestJpegContent();
    $remote_url = 'https://example.com/test-image.jpg';

    // Mock successful HTTP response.
    $mock_handler = new MockHandler([
      // First response for HEAD request (content type check).
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => strlen($image_content),
      ]),
      // Second response for GET request (actual download).
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => strlen($image_content),
      ], $image_content),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    // Test remote download.
    $public_uri = 'public://downloaded-image.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertEquals($public_uri, $result, 'Remote download should succeed');
    $this->assertFileExists($this->container->get('file_system')->realpath($public_uri));

    // Verify file content matches.
    $downloaded_content = file_get_contents($this->container->get('file_system')->realpath($public_uri));
    $this->assertEquals($image_content, $downloaded_content, 'Downloaded content should match original');
  }

  /**
   * Tests handling of HTTP client timeout errors.
   */
  public function testHttpTimeoutHandling(): void {
    $remote_url = 'https://slow-server.example.com/image.jpg';

    // Mock timeout exception.
    $mock_handler = new MockHandler([
      new ConnectException('Connection timed out after 30 seconds', new Request('GET', $remote_url)),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://timeout-test.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'Timeout should return error message');
    $this->assertStringContainsString('Connection timed out', $result);
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests handling of HTTP 4xx client errors.
   */
  public function testHttpClientErrorHandling(): void {
    $remote_url = 'https://example.com/not-found.jpg';

    $mock_handler = new MockHandler([
      new Response(404, ['Content-Type' => 'text/html'], '<html><body>Not Found</body></html>'),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://not-found.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, '404 error should return error message');
    $this->assertStringContainsString('404', $result);
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests handling of HTTP 5xx server errors.
   */
  public function testHttpServerErrorHandling(): void {
    $remote_url = 'https://broken-server.example.com/image.jpg';

    $mock_handler = new MockHandler([
      new Response(500, ['Content-Type' => 'text/html'], '<html><body>Internal Server Error</body></html>'),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://server-error.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, '500 error should return error message');
    $this->assertStringContainsString('500', $result);
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests handling of malformed HTTP responses.
   */
  public function testMalformedHttpResponseHandling(): void {
    $remote_url = 'https://malformed.example.com/image.jpg';

    // Mock malformed response (claims to be image but isn't).
    $mock_handler = new MockHandler([
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => '100',
      ]),
      new Response(200, [
        'Content-Type' => 'image/jpeg',
      ], '<html><body>This is not an image</body></html>'),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://malformed.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    // Should fail content verification and return error message.
    $this->assertStringContainsString('Downloaded file failed content verification', $result);
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests HTTP redirect following behavior.
   */
  public function testHttpRedirectFollowing(): void {
    $original_url = 'https://example.com/redirect-me.jpg';
    $final_url = 'https://cdn.example.com/actual-image.jpg';
    $image_content = $this->createTestJpegContent();

    $mock_handler = new MockHandler([
      // Initial HEAD request - redirect.
      new Response(301, [
        'Location' => $final_url,
        'Content-Type' => 'text/html',
      ]),
      // Follow-up HEAD request to final URL.
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => strlen($image_content),
      ]),
      // GET request - redirect.
      new Response(301, [
        'Location' => $final_url,
        'Content-Type' => 'text/html',
      ]),
      // Follow-up GET request to final URL.
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => strlen($image_content),
      ], $image_content),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://redirected-image.jpg';
    $result = $this->swapperService->downloadRemoteFile($original_url, $public_uri);

    $this->assertEquals($public_uri, $result, 'Redirected download should succeed');
    $this->assertFileExists($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests handling of too many redirects.
   */
  public function testTooManyRedirectsHandling(): void {
    $remote_url = 'https://redirect-loop.example.com/image.jpg';

    // Create a redirect loop that exceeds the max_redirects setting.
    $responses = [];
    for ($i = 0; $i < 10; $i++) {
      $responses[] = new Response(301, [
        'Location' => 'https://redirect-loop.example.com/redirect-' . ($i + 1) . '.jpg',
      ]);
    }

    $mock_handler = new MockHandler($responses);
    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://redirect-loop.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'Too many redirects should return error message');
    $this->assertStringContainsString('redirect', strtolower($result));
  }

  /**
   * Tests handling of oversized file downloads.
   */
  public function testOversizedFileHandling(): void {
    $remote_url = 'https://example.com/huge-file.jpg';

    // Set a small file size limit for testing.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('max_file_size', 1);
    $config->save();

    // Mock response claiming to be 10MB.
    $mock_handler = new MockHandler([
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => '10485760',
      ]),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://huge-file.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'Oversized file should return error message');
    $this->assertStringContainsString('file too large', strtolower($result));
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests handling of invalid content types.
   */
  public function testInvalidContentTypeHandling(): void {
    $remote_url = 'https://example.com/malicious-script.jpg';

    $mock_handler = new MockHandler([
      new Response(200, [
        'Content-Type' => 'text/html',
        'Content-Length' => '100',
      ]),
      new Response(200, [
        'Content-Type' => 'text/html',
      ], '<script>alert("xss")</script>'),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://malicious-script.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'Invalid content type should return error message');
    $this->assertStringContainsString('mime type', strtolower($result));
  }

  /**
   * Tests network connectivity issues.
   */
  public function testNetworkConnectivityIssues(): void {
    $remote_url = 'https://unreachable.example.com/image.jpg';

    $mock_handler = new MockHandler([
      new ConnectException('Could not resolve host', new Request('GET', $remote_url)),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://unreachable.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'Network error should return error message');
    $this->assertStringContainsString('Could not resolve host', $result);
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests SSL certificate issues.
   */
  public function testSslCertificateIssues(): void {
    $remote_url = 'https://invalid-cert.example.com/image.jpg';

    $mock_handler = new MockHandler([
      new RequestException(
        'SSL certificate problem: unable to get local issuer certificate',
        new Request('GET', $remote_url)
      ),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://ssl-error.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'SSL error should return error message');
    $this->assertStringContainsString('SSL certificate', $result);
  }

  /**
   * Tests concurrent download handling.
   */
  public function testConcurrentDownloadHandling(): void {
    $urls_and_responses = [
      'https://fast.example.com/image1.jpg' => $this->createTestJpegContent(),
      'https://medium.example.com/image2.png' => $this->createTestPngContent(),
      'https://slow.example.com/image3.gif' => $this->createTestGifContent(),
    ];

    $responses = [];
    foreach ($urls_and_responses as $url => $content) {
      $mime_type = strpos($url, '.jpg') ? 'image/jpeg' :
                  (strpos($url, '.png') ? 'image/png' : 'image/gif');

      // HEAD response.
      $responses[] = new Response(200, [
        'Content-Type' => $mime_type,
        'Content-Length' => strlen($content),
      ]);

      // GET response.
      $responses[] = new Response(200, [
        'Content-Type' => $mime_type,
      ], $content);
    }

    $mock_handler = new MockHandler($responses);
    $this->replaceMockHttpClient($mock_handler);

    $results = [];
    $i = 1;
    foreach ($urls_and_responses as $url => $expected_content) {
      $public_uri = "public://concurrent-{$i}.jpg";
      $results[$url] = $this->swapperService->downloadRemoteFile($url, $public_uri);
      $i++;
    }

    // All downloads should succeed.
    foreach ($results as $url => $result) {
      $this->assertStringStartsWith('public://', $result, "Download should succeed for {$url}");
    }
  }

  /**
   * Tests handling of unexpected exceptions during HTTP operations.
   */
  public function testUnexpectedExceptionHandling(): void {
    $remote_url = 'https://exception.example.com/image.jpg';

    $mock_handler = new MockHandler([
      new TransferException('Unexpected transfer error occurred'),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    $public_uri = 'public://exception-test.jpg';
    $result = $this->swapperService->downloadRemoteFile($remote_url, $public_uri);

    $this->assertIsString($result, 'Unexpected exception should return error message');
    $this->assertStringContainsString('Unexpected transfer error', $result);
    $this->assertFileDoesNotExist($this->container->get('file_system')->realpath($public_uri));
  }

  /**
   * Tests complete remote file to media workflow with mocked HTTP.
   */
  public function testCompleteRemoteFileToMediaWorkflow(): void {
    $remote_url = 'https://example.com/workflow-test.jpg';
    $image_content = $this->createTestJpegContent();

    $mock_handler = new MockHandler([
      new Response(200, [
        'Content-Type' => 'image/jpeg',
        'Content-Length' => strlen($image_content),
      ]),
      new Response(200, [
        'Content-Type' => 'image/jpeg',
      ], $image_content),
    ]);

    $this->replaceMockHttpClient($mock_handler);

    // Test complete workflow from URL to media entity.
    $media = $this->swapperService->validateAndProcessRemoteFile($remote_url);

    $this->assertInstanceOf('Drupal\media\MediaInterface', $media,
      'Remote file should be converted to media entity');
    $this->assertEquals('image', $media->bundle(),
      'Media should use correct bundle for image');
    $this->assertStringContainsString('workflow-test', strtolower($media->getName()),
      'Media name should be based on filename');
  }

  /**
   * Creates test JPEG content with valid header.
   */
  protected function createTestJpegContent(): string {
    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 {
    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 {
    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;";
  }

  /**
   * Replaces the HTTP client in the SwapperService with a mock.
   */
  protected function replaceMockHttpClient(MockHandler $mockHandler): void {
    $handlerStack = HandlerStack::create($mockHandler);
    $mockClient = new Client(['handler' => $handlerStack]);

    // Get the service container.
    $container = $this->container;

    // Replace the HTTP client service.
    $container->set('http_client', $mockClient);

    // Recreate the SwapperService with the new HTTP client.
    $this->swapperService = new SwapperService(
      $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'),
    );
  }

}

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

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