inline_image_saver-1.0.x-dev/tests/src/Kernel/InlineImageValidationTest.php
tests/src/Kernel/InlineImageValidationTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\inline_image_saver\Kernel;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\file\FileInterface;
use Drupal\inline_image_saver\Form\InlineImageSaverSettingsForm;
use Drupal\inline_image_saver\Struct\InlineImageError;
use Drupal\inline_image_saver\Plugin\Validation\Constraint\InlineImageSaverConstraint;
use Drupal\KernelTests\Core\File\FileTestBase;
use Drupal\node\NodeInterface;
use Drupal\Tests\inline_image_saver\Traits\InlineImageTestTrait;
use Drupal\Tests\inline_image_saver\Traits\SetupBaseUrlTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests the inline_image_saver module.
*/
class InlineImageValidationTest extends FileTestBase {
use TestFileCreationTrait;
use ContentTypeCreationTrait;
use UserCreationTrait;
use InlineImageTestTrait;
use SetupBaseUrlTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'node',
'field',
'file',
'text',
'editor',
'filter',
'filter_test',
'inline_image_saver',
];
/**
* The default module settings.
*/
protected array $defaultModuleSettings;
/**
* Test cases for different validation scenarios.
*
* @var array<string, array{
* expected_error: InlineImageError,
* is_src_downloadable: bool,
* img_attributes: array<string, string>,
* settings_for_success: array<string, mixed>,
* settings_for_failure: array<string, mixed>
* }>
*/
protected array $validationTestCases;
/**
* Tracks the number of times each error type has been tested.
*
* @var array<string, int>
*/
protected array $testedErrors;
/**
* The node type being tested.
*/
protected string $nodeTypeId;
/**
* The markup field name being tested.
*/
protected string $markupFieldName;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setupBaseUrl();
$this->installEntitySchema('node');
$this->installSchema('node', 'node_access');
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
$this->installConfig(['node', 'filter', 'filter_test', 'inline_image_saver']);
$this->processableFormatId = 'full_html';
$this->setUpCurrentUser([], ["use text format $this->processableFormatId"]);
$this->nodeTypeId = $this->createContentType()->id();
$this->markupFieldName = 'body';
$this->defaultModuleSettings = [
'processable_formats' => [$this->processableFormatId],
'validation_settings' => [
'allow_if_downloadable' => FALSE,
'allow_data_uri' => FALSE,
'check_file_exists' => FALSE,
'validate_url' => FALSE,
'validate_url_query' => FALSE,
'check_file_mime' => FALSE,
],
'fallback_markup' => '<s>' . Html::escape('<img src="@src" alt="@alt" title="@title" data-entity-type="@data-entity-type" data-entity-uuid="@data-entity-uuid">') . '</s>',
'create_revision' => FALSE,
'revision_log' => $this->randomMachineName(),
'skip_on_sync' => FALSE,
];
}
/**
* Tests the validation.
*/
public function testValidation(): void {
$inline_image_saver = $this->container->get('inline_image_saver');
$img_entity_type_id = 'file';
$images = $this->getTestFiles('image');
$file_storage = $this->container->get('entity_type.manager')->getStorage($img_entity_type_id);
// Prepare valid image file.
$img_file = $file_storage->create((array) $images[0]);
$img_file->save();
$this->assertInstanceOf(FileInterface::class, $img_file);
$img_src = $img_file->createFileUrl();
$img_uuid = $img_file->uuid();
// Prepare invalid image file - deleted file.
$missing_image_file = $file_storage->create((array) $images[1]);
$missing_image_file->save();
$this->assertInstanceOf(FileInterface::class, $missing_image_file);
$this->container->get('file_system')->unlink($missing_image_file->getFileUri());
// Prepare invalid image file - wrong MIME type.
$non_image_file = $file_storage->create((array) $this->getTestFiles('text')[0]);
$non_image_file->save();
$this->assertInstanceOf(FileInterface::class, $non_image_file);
$this->addValidationTestCase(
InlineImageError::InvalidDataUriValue,
img_attributes: ['src' => 'data:foo'],
settings_for_failure: ['allow_data_uri' => FALSE],
settings_for_success: ['allow_data_uri' => TRUE],
);
$this->addValidationTestCase(
InlineImageError::UnsupportedDataUriMimeType,
img_attributes: ['src' => 'data:image/gif;base64,foo'],
settings_for_failure: ['allow_data_uri' => FALSE],
settings_for_success: ['allow_data_uri' => TRUE],
);
$this->addValidationTestCase(
InlineImageError::DataUriNotAllowed,
img_attributes: ['src' => 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='],
settings_for_failure: ['allow_data_uri' => FALSE],
settings_for_success: ['allow_data_uri' => TRUE],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(InlineImageError::EmptyEntityTypeAttribute);
$this->addValidationTestCase(
InlineImageError::EmptyEntityTypeAttribute,
img_attributes: ['src' => $img_src],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(
InlineImageError::UnsupportedEntityTypeAttribute,
img_attributes: [
'src' => $img_src,
'data-entity-type' => $this->randomMachineName(),
],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(
InlineImageError::EmptyEntityUuid,
img_attributes: [
'src' => $img_src,
'data-entity-type' => $img_entity_type_id,
],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(
InlineImageError::EntityNotFound,
img_attributes: [
'src' => $img_src,
'data-entity-type' => $img_entity_type_id,
'data-entity-uuid' => 'foo',
],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(
InlineImageError::FileNotFound,
img_attributes: [
'src' => $img_src,
'data-entity-type' => $img_entity_type_id,
'data-entity-uuid' => $missing_image_file->uuid(),
],
settings_for_failure: ['check_file_exists' => TRUE],
settings_for_success: ['check_file_exists' => FALSE],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(
InlineImageError::UrlHostMismatch,
img_attributes: [
'src' => "https://{$this->randomMachineName()}.com/$img_src",
'data-entity-type' => $img_entity_type_id,
'data-entity-uuid' => $img_uuid,
],
settings_for_failure: ['validate_url' => TRUE],
settings_for_success: ['validate_url' => FALSE],
);
$this->addValidationTestCase(
InlineImageError::UrlPathMismatch,
img_attributes: [
'src' => "$img_src/foo",
'data-entity-type' => $img_entity_type_id,
'data-entity-uuid' => $img_uuid,
],
settings_for_failure: ['validate_url' => TRUE],
settings_for_success: ['validate_url' => FALSE],
);
$this->addValidationTestCase(
InlineImageError::UrlQueryMismatch,
img_attributes: [
'src' => "$img_src?foo",
'data-entity-type' => $img_entity_type_id,
'data-entity-uuid' => $img_uuid,
],
settings_for_failure: [
'validate_url' => TRUE,
'validate_url_query' => TRUE,
],
settings_for_success: [
'validate_url' => TRUE,
'validate_url_query' => FALSE,
],
is_src_downloadable: TRUE,
);
$this->addValidationTestCase(
InlineImageError::UnsupportedFileMime,
img_attributes: [
'src' => $non_image_file->createFileUrl(),
'data-entity-type' => $img_entity_type_id,
'data-entity-uuid' => $non_image_file->uuid(),
],
settings_for_failure: ['check_file_mime' => TRUE],
settings_for_success: ['check_file_mime' => FALSE],
);
$this->testedErrors[InlineImageError::Unknown->name] ??= 1;
$this->assertCount(count(InlineImageError::cases()), $this->testedErrors, 'Untestable errors found.');
$constraint = $this->container->get('validation.constraint')->create('InlineImageSaver', NULL);
$this->assertInstanceOf(InlineImageSaverConstraint::class, $constraint);
foreach ($this->validationTestCases as $case) {
$value_prop = $this->createNodeWithImage($case['img_attributes'])->get($this->markupFieldName)->first();
// Test validation.
$settings = [
'enable_validation' => TRUE,
'validation_settings' => [
'allow_if_downloadable' => FALSE,
],
'enable_download' => TRUE,
];
$this->updateModuleSettings($case['settings_for_failure'], $settings);
$violations = $value_prop->validate();
$this->assertCount(1, $violations);
$error_name = $case['expected_error']->name;
$expected_message = $constraint->{"message$error_name"};
$actual_message = $violations->get(0)->getMessage();
$this->assertInstanceOf(TranslatableMarkup::class, $actual_message);
$this->assertEquals($expected_message, $actual_message->getUntranslatedString());
// Test validation pass.
if ($case['settings_for_success']) {
$this->updateModuleSettings($case['settings_for_success'], $settings);
$violations = $value_prop->validate();
$this->assertCount(0, $violations);
}
// Test validation pass if downloadable.
if ($case['is_src_downloadable']) {
$this->updateModuleSettings($case['settings_for_failure'], $settings, [
'validation_settings' => ['allow_if_downloadable' => TRUE],
]);
$violations = $value_prop->validate();
$this->assertCount(0, $violations);
}
}
// Test that entities are not processed when syncing is enabled.
$settings = $this->updateModuleSettings([
'enable_validation' => TRUE,
'validation_settings' => [
'allow_if_downloadable' => TRUE,
'allow_data_uri' => TRUE,
'check_file_exists' => TRUE,
'validate_url' => TRUE,
'validate_url_query' => TRUE,
'check_file_mime' => TRUE,
],
'enable_download' => TRUE,
'prefer_reuse_files' => FALSE,
'enable_replace' => TRUE,
'create_revision' => TRUE,
'skip_on_sync' => TRUE,
]);
$node = $this->createNodeWithImage(['src' => $img_src]);
$value_prop = $node->get($this->markupFieldName)->first()->get('value');
$body_expected = $value_prop->getString();
$node->setSyncing(TRUE)->save();
$this->assertEquals($body_expected, $value_prop->getString());
$node->setSyncing(FALSE);
// Test format exclusion logic.
$format_prop = $node->get($this->markupFieldName)->first()->get('format');
$format_prop->setValue('filtered_html');
$node->save();
$this->assertEquals($body_expected, $value_prop->getString());
$format_prop->setValue($this->processableFormatId);
// Test file download.
$current_revision_id = $node->getRevisionId();
$node->save();
$img = $this->extractImageElement($value_prop->getString());
$validation = $inline_image_saver->validateImage($img);
$this->assertEquals(NULL, $validation->error);
$this->assertInstanceOf(FileInterface::class, $validation->file);
$this->assertTrue($validation->file->isPermanent());
$actual_uuid = $validation->file->uuid();
$this->assertEquals($actual_uuid, $img->getAttribute('data-entity-uuid'));
$this->assertNotEquals($actual_uuid, $img_uuid);
$this->assertEquals($validation->file->getEntityTypeId(), $img->getAttribute('data-entity-type'));
$this->assertNotEquals($current_revision_id, $node->getRevisionId());
$this->assertEquals($settings['revision_log'], $node->getRevisionLogMessage());
// Test fallback replacement.
$current_revision_id = $node->getRevisionId();
$value_prop->setValue($this->createImageMarkup([
'src' => $this->randomMachineName() . '&',
'alt' => $this->randomMachineName() . '&',
'title' => $this->randomMachineName() . '&',
'data-entity-type' => $this->randomMachineName() . '&',
'data-entity-uuid' => $this->randomMachineName() . '&',
]));
$img = $this->extractImageElement($value_prop->getString());
$node->save();
$placeholders = $inline_image_saver->placeholdersFromElement($img);
$fallback_markup = $inline_image_saver->processPlaceholders($settings['fallback_markup'], $placeholders);
$this->assertStringContainsString(Html::normalize($fallback_markup), Html::normalize($value_prop->getString()));
$this->assertNotEquals($current_revision_id, $node->getRevisionId());
$this->assertEquals($settings['revision_log'], $node->getRevisionLogMessage());
// Test file reuse.
$settings['prefer_reuse_files'] = TRUE;
$this->updateModuleSettings($settings);
$value_prop->setValue($this->createImageMarkup(['src' => $img_src]));
$node->save();
$img = $this->extractImageElement($value_prop->getString());
$this->assertEquals($img_uuid, $img->getAttribute('data-entity-uuid'));
}
/**
* Adds a validation test case.
*/
protected function addValidationTestCase(InlineImageError $expected_error, array $img_attributes = [], array $settings_for_failure = [], array $settings_for_success = [], bool $is_src_downloadable = FALSE): void {
$error_name = $expected_error->name;
$count = &$this->testedErrors[$error_name];
$count++;
$error_key = $error_name;
if ($count > 1) {
$error_key .= "_$count";
}
$img_attributes['alt'] ??= $error_name;
$case = [
'expected_error' => $expected_error,
'img_attributes' => $img_attributes,
'is_src_downloadable' => $is_src_downloadable,
];
foreach ([
'settings_for_failure' => $settings_for_failure,
'settings_for_success' => $settings_for_success,
] as $type => $settings) {
$case[$type] = [];
foreach ($settings as $key => $value) {
$case[$type]['validation_settings'][$key] = $value;
}
}
$this->validationTestCases[$error_key] = $case;
}
/**
* Creates a node with image markup.
*/
protected function createNodeWithImage(array $attributes): NodeInterface {
return $this->container->get('entity_type.manager')->getStorage('node')->create([
'type' => $this->nodeTypeId,
'title' => $this->randomMachineName(),
$this->markupFieldName => [
'value' => $this->createImageMarkup($attributes),
'format' => $this->processableFormatId,
],
]);
}
/**
* Updates the module settings.
*/
protected function updateModuleSettings(array ...$settings): array {
$main_settings = [
'enable_validation',
'enable_download',
];
$settings = NestedArray::mergeDeep($this->defaultModuleSettings, ...$settings);
$this->assertEqualsCanonicalizing(
$main_settings,
array_intersect($main_settings, array_keys($settings)),
'The configuration must include the main settings.'
);
return $this->config(InlineImageSaverSettingsForm::CONFIG_NAME)
->setData($settings)
->save()
->get();
}
}
