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