Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,105 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Action\ActionUnitTest.
*/
namespace Drupal\system\Tests\Action;
use Drupal\simpletest\KernelTestBase;
use Drupal\Core\Action\ActionInterface;
use Drupal\user\RoleInterface;
/**
* Tests action plugins.
*
* @group Action
*/
class ActionUnitTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('system', 'field', 'user', 'action_test');
/**
* The action manager.
*
* @var \Drupal\Core\Action\ActionManager
*/
protected $actionManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->actionManager = $this->container->get('plugin.manager.action');
$this->installEntitySchema('user');
$this->installSchema('system', array('sequences'));
}
/**
* Tests the functionality of test actions.
*/
public function testOperations() {
// Test that actions can be discovered.
$definitions = $this->actionManager->getDefinitions();
$this->assertTrue(count($definitions) > 1, 'Action definitions are found.');
$this->assertTrue(!empty($definitions['action_test_no_type']), 'The test action is among the definitions found.');
$definition = $this->actionManager->getDefinition('action_test_no_type');
$this->assertTrue(!empty($definition), 'The test action definition is found.');
$definitions = $this->actionManager->getDefinitionsByType('user');
$this->assertTrue(empty($definitions['action_test_no_type']), 'An action with no type is not found.');
// Create an instance of the 'save entity' action.
$action = $this->actionManager->createInstance('action_test_save_entity');
$this->assertTrue($action instanceof ActionInterface, 'The action implements the correct interface.');
// Create a new unsaved user.
$name = $this->randomMachineName();
$user_storage = $this->container->get('entity.manager')->getStorage('user');
$account = $user_storage->create(array('name' => $name, 'bundle' => 'user'));
$loaded_accounts = $user_storage->loadMultiple();
$this->assertEqual(count($loaded_accounts), 0);
// Execute the 'save entity' action.
$action->execute($account);
$loaded_accounts = $user_storage->loadMultiple();
$this->assertEqual(count($loaded_accounts), 1);
$account = reset($loaded_accounts);
$this->assertEqual($name, $account->label());
}
/**
* Tests the dependency calculation of actions.
*/
public function testDependencies() {
// Create a new action that depends on a user role.
$action = entity_create('action', array(
'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID,
'type' => 'user',
'label' => t('Add the anonymous role to the selected users'),
'configuration' => array(
'rid' => RoleInterface::ANONYMOUS_ID,
),
'plugin' => 'user_add_role_action',
));
$action->save();
$expected = array(
'config' => array(
'user.role.' . RoleInterface::ANONYMOUS_ID,
),
'module' => array(
'user',
),
);
$this->assertIdentical($expected, $action->calculateDependencies());
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxFormCacheTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
/**
* Tests the usage of form caching for AJAX forms.
*
* @group Ajax
*/
class AjaxFormCacheTest extends AjaxTestBase {
/**
* Tests the usage of form cache for AJAX forms.
*/
public function testFormCacheUsage() {
/** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
$key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');
$this->drupalLogin($this->rootUser);
// Ensure that the cache is empty.
$this->assertEqual(0, count($key_value_expirable->getAll()));
// Visit an AJAX form that is not cached, 3 times.
$uncached_form_url = Url::fromRoute('ajax_forms_test.commands_form');
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
// The number of cache entries should not have changed.
$this->assertEqual(0, count($key_value_expirable->getAll()));
// Visit a form that is explicitly cached, 3 times.
$cached_form_url = Url::fromRoute('ajax_forms_test.cached_form');
$this->drupalGet($cached_form_url);
$this->drupalGet($cached_form_url);
$this->drupalGet($cached_form_url);
// The number of cache entries should be exactly 3.
$this->assertEqual(3, count($key_value_expirable->getAll()));
}
/**
* Tests AJAX forms in blocks.
*/
public function testBlockForms() {
$this->container->get('module_installer')->install(['block', 'search']);
$this->rebuildContainer();
$this->container->get('router.builder')->rebuild();
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('search_form_block', ['weight' => -5]);
$this->drupalPlaceBlock('ajax_forms_test_block', ['cache' => ['max_age' => 0]]);
$this->drupalGet('');
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
$this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
$this->assertOptionWithDrupalSelector('edit-test1', 'option3');
$this->drupalPostForm(NULL, ['test1' => 'option1'], 'Submit');
$this->assertText('Submission successful.');
}
/**
* Tests AJAX forms on pages with a query string.
*/
public function testQueryString() {
$this->container->get('module_installer')->install(['block']);
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('ajax_forms_test_block', ['cache' => ['max_age' => 0]]);
$url = Url::fromRoute('entity.user.canonical', ['user' => $this->rootUser->id()], ['query' => ['foo' => 'bar']]);
$this->drupalGet($url);
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
$url->setOption('query', [
'foo' => 'bar',
FormBuilderInterface::AJAX_FORM_REQUEST => 1,
MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
]);
$this->assertUrl($url);
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxFormPageCacheTest.
*/
namespace Drupal\system\Tests\Ajax;
/**
* Performs tests on AJAX forms in cached pages.
*
* @group Ajax
*/
class AjaxFormPageCacheTest extends AjaxTestBase {
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Return the build id of the current form.
*/
protected function getFormBuildId() {
$build_id_fields = $this->xpath('//input[@name="form_build_id"]');
$this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page');
return (string) $build_id_fields[0]['value'];
}
/**
* Create a simple form, then POST to system/ajax to change to it.
*/
public function testSimpleAJAXFormValue() {
$this->drupalGet('ajax_forms_test_get_form');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
$build_id_initial = $this->getFormBuildId();
$edit = ['select' => 'green'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_first_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_initial, $build_id_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_initial,
'new' => $build_id_first_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
$edit = ['select' => 'red'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_second_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id changes on subsequent AJAX submissions');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_first_ajax,
'new' => $build_id_second_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
// Repeat the test sequence but this time with a page loaded from the cache.
$this->drupalGet('ajax_forms_test_get_form');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
$build_id_from_cache_initial = $this->getFormBuildId();
$this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request');
$edit = ['select' => 'green'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_from_cache_first_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_from_cache_initial, $build_id_from_cache_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission');
$this->assertNotEqual($build_id_first_ajax, $build_id_from_cache_first_ajax, 'Build id from first user is not reused');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_from_cache_initial,
'new' => $build_id_from_cache_first_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
$edit = ['select' => 'red'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_from_cache_second_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id changes on subsequent AJAX submissions');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_from_cache_first_ajax,
'new' => $build_id_from_cache_second_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
}
/**
* Tests a form that uses an #ajax callback.
*
* @see \Drupal\system\Tests\Ajax\ElementValidationTest::testAjaxElementValidation()
*/
public function testAjaxElementValidation() {
$edit = ['drivertext' => t('some dumb text')];
$this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxTestBase.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\simpletest\WebTestBase;
/**
* Provides a base class for Ajax tests.
*/
abstract class AjaxTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'ajax_test', 'ajax_forms_test');
/**
* Asserts the array of Ajax commands contains the searched command.
*
* An AjaxResponse object stores an array of Ajax commands. This array
* sometimes includes commands automatically provided by the framework in
* addition to commands returned by a particular controller. During testing,
* we're usually interested that a particular command is present, and don't
* care whether other commands precede or follow the one we're interested in.
* Additionally, the command we're interested in may include additional data
* that we're not interested in. Therefore, this function simply asserts that
* one of the commands in $haystack contains all of the keys and values in
* $needle. Furthermore, if $needle contains a 'settings' key with an array
* value, we simply assert that all keys and values within that array are
* present in the command we're checking, and do not consider it a failure if
* the actual command contains additional settings that aren't part of
* $needle.
*
* @param $haystack
* An array of rendered Ajax commands returned by the server.
* @param $needle
* Array of info we're expecting in one of those commands.
* @param $message
* An assertion message.
*/
protected function assertCommand($haystack, $needle, $message) {
$found = FALSE;
foreach ($haystack as $command) {
// If the command has additional settings that we're not testing for, do
// not consider that a failure.
if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
$command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
}
// If the command has additional data that we're not testing for, do not
// consider that a failure. Also, == instead of ===, because we don't
// require the key/value pairs to be in any particular order
// (http://www.php.net/manual/language.operators.array.php).
if (array_intersect_key($command, $needle) == $needle) {
$found = TRUE;
break;
}
}
$this->assertTrue($found, $message);
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\CommandsTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AfterCommand;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\BeforeCommand;
use Drupal\Core\Ajax\ChangedCommand;
use Drupal\Core\Ajax\CssCommand;
use Drupal\Core\Ajax\DataCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\RestripeCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Performs tests on AJAX framework commands.
*
* @group Ajax
*/
class CommandsTest extends AjaxTestBase {
/**
* Tests the various Ajax Commands.
*/
function testAjaxCommands() {
$form_path = 'ajax_forms_test_ajax_commands_form';
$web_user = $this->drupalCreateUser(array('access content'));
$this->drupalLogin($web_user);
$edit = array();
// Tests the 'add_css' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'add_css' command")));
$expected = new AddCssCommand('my/file.css');
$this->assertCommand($commands, $expected->render(), "'add_css' AJAX command issued with correct data.");
// Tests the 'after' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div")));
$expected = new AfterCommand('#after_div', 'This will be placed after');
$this->assertCommand($commands, $expected->render(), "'after' AJAX command issued with correct data.");
// Tests the 'alert' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert")));
$expected = new AlertCommand(t('Alert'));
$this->assertCommand($commands, $expected->render(), "'alert' AJAX Command issued with correct text.");
// Tests the 'append' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something")));
$expected = new AppendCommand('#append_div', 'Appended text');
$this->assertCommand($commands, $expected->render(), "'append' AJAX command issued with correct data.");
// Tests the 'before' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div")));
$expected = new BeforeCommand('#before_div', 'Before text');
$this->assertCommand($commands, $expected->render(), "'before' AJAX command issued with correct data.");
// Tests the 'changed' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed.")));
$expected = new ChangedCommand('#changed_div');
$this->assertCommand($commands, $expected->render(), "'changed' AJAX command issued with correct selector.");
// Tests the 'changed' command using the second argument.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk.")));
$expected = new ChangedCommand('#changed_div', '#changed_div_mark_this');
$this->assertCommand($commands, $expected->render(), "'changed' AJAX command (with asterisk) issued with correct selector.");
// Tests the 'css' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("Set the '#box' div to be blue.")));
$expected = new CssCommand('#css_div', array('background-color' => 'blue'));
$this->assertCommand($commands, $expected->render(), "'css' AJAX command issued with correct selector.");
// Tests the 'data' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX data command: Issue command.")));
$expected = new DataCommand('#data_div', 'testkey', 'testvalue');
$this->assertCommand($commands, $expected->render(), "'data' AJAX command issued with correct key and value.");
// Tests the 'html' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector.")));
$expected = new HtmlCommand('#html_div', 'replacement text');
$this->assertCommand($commands, $expected->render(), "'html' AJAX command issued with correct data.");
// Tests the 'insert' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX insert: Let client insert based on #ajax['method'].")));
$expected = new InsertCommand('#insert_div', 'insert replacement text');
$this->assertCommand($commands, $expected->render(), "'insert' AJAX command issued with correct data.");
// Tests the 'invoke' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX invoke command: Invoke addClass() method.")));
$expected = new InvokeCommand('#invoke_div', 'addClass', array('error'));
$this->assertCommand($commands, $expected->render(), "'invoke' AJAX command issued with correct method and argument.");
// Tests the 'prepend' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something")));
$expected = new PrependCommand('#prepend_div', 'prepended text');
$this->assertCommand($commands, $expected->render(), "'prepend' AJAX command issued with correct data.");
// Tests the 'remove' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text")));
$expected = new RemoveCommand('#remove_text');
$this->assertCommand($commands, $expected->render(), "'remove' AJAX command issued with correct command and selector.");
// Tests the 'restripe' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'restripe' command")));
$expected = new RestripeCommand('#restripe_table');
$this->assertCommand($commands, $expected->render(), "'restripe' AJAX command issued with correct selector.");
// Tests the 'settings' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'settings' command")));
$expected = new SettingsCommand(array('ajax_forms_test' => array('foo' => 42)));
$this->assertCommand($commands, $expected->render(), "'settings' AJAX command issued with correct data.");
}
/**
* Regression test: Settings command exists regardless of JS aggregation.
*/
public function testAttachedSettings() {
$assert = function($message) {
$response = new AjaxResponse();
$response->setAttachments([
'library' => ['core/drupalSettings'],
'drupalSettings' => ['foo' => 'bar'],
]);
$ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
$subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
$event = new FilterResponseEvent(
\Drupal::service('http_kernel'),
new Request(),
HttpKernelInterface::MASTER_REQUEST,
$response
);
$subscriber->onResponse($event);
$expected = [
'command' => 'settings',
];
$this->assertCommand($response->getCommands(), $expected, $message);
};
$config = $this->config('system.performance');
$config->set('js.preprocess', FALSE)->save();
$assert('Settings command exists when JS aggregation is disabled.');
$config->set('js.preprocess', TRUE)->save();
$assert('Settings command exists when JS aggregation is enabled.');
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\DialogTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
/**
* Performs tests on opening and manipulating dialogs via AJAX commands.
*
* @group Ajax
*/
class DialogTest extends AjaxTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('ajax_test', 'ajax_forms_test', 'contact');
/**
* Test sending non-JS and AJAX requests to open and manipulate modals.
*/
public function testDialog() {
$this->drupalLogin($this->drupalCreateUser(array('administer contact forms')));
// Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog');
// Set up variables for this test.
$dialog_renderable = \Drupal\ajax_test\Controller\AjaxTestController::dialogContents();
$dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable);
$modal_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' => array(
'modal' => TRUE,
'title' => 'AJAX Dialog contents',
),
);
$form_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'dialogOptions' => array(
'modal' => TRUE,
'title' => 'Ajax Form contents',
),
);
$entity_form_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'dialogOptions' => array(
'modal' => TRUE,
'title' => 'Add contact form',
),
);
$normal_expected_response = array(
'command' => 'openDialog',
'selector' => '#ajax-test-dialog-wrapper-1',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' => array(
'modal' => FALSE,
'title' => 'AJAX Dialog contents',
),
);
$no_target_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-dialog-ajax-testdialog-contents',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' => array(
'modal' => FALSE,
'title' => 'AJAX Dialog contents',
),
);
$close_expected_response = array(
'command' => 'closeDialog',
'selector' => '#ajax-test-dialog-wrapper-1',
'persist' => FALSE,
);
// Check that requesting a modal dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-contents');
$this->assertRaw($dialog_contents, 'Non-JS modal dialog page present.');
// Emulate going to the JS version of the page and check the JSON response.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$this->assertEqual($modal_expected_response, $ajax_result[3], 'Modal dialog JSON response matches.');
// Check that requesting a "normal" dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-contents');
$this->assertRaw($dialog_contents, 'Non-JS normal dialog page present.');
// Emulate going to the JS version of the page and check the JSON response.
// This needs to use WebTestBase::drupalPostAjaxForm() so that the correct
// dialog options are sent.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
// We have to mock a form element to make drupalPost submit from a link.
'textfield' => 'test',
), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')), array(), NULL, array(
'submit' => array(
'dialogOptions[target]' => 'ajax-test-dialog-wrapper-1',
)
));
$this->assertEqual($normal_expected_response, $ajax_result[3], 'Normal dialog JSON response matches.');
// Emulate going to the JS version of the page and check the JSON response.
// This needs to use WebTestBase::drupalPostAjaxForm() so that the correct
// dialog options are sent.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
// We have to mock a form element to make drupalPost submit from a link.
'textfield' => 'test',
), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')), array(), NULL, array(
// Don't send a target.
'submit' => array()
));
// Make sure the selector ID starts with the right string.
$this->assert(strpos($ajax_result[3]['selector'], $no_target_expected_response['selector']) === 0, 'Selector starts with right string.');
unset($ajax_result[3]['selector']);
unset($no_target_expected_response['selector']);
$this->assertEqual($no_target_expected_response, $ajax_result[3], 'Normal dialog with no target JSON response matches.');
// Emulate closing the dialog via an AJAX request. There is no non-JS
// version of this test.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-close');
$this->assertEqual($close_expected_response, $ajax_result[0], 'Close dialog JSON response matches.');
// Test submitting via a POST request through the button for modals. This
// approach more accurately reflects the real responses by Drupal because
// all of the necessary page variables are emulated.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(), 'button1');
// Check that CSS and JavaScript are "added" to the page dynamically.
$this->assertTrue(in_array('core/drupal.dialog.ajax', explode(',', $ajax_result[0]['settings']['ajaxPageState']['libraries'])), 'core/drupal.dialog.ajax library is added to the page.');
$dialog_css_exists = strpos($ajax_result[1]['data'], 'dialog.css') !== FALSE;
$this->assertTrue($dialog_css_exists, 'jQuery UI dialog CSS added to the page.');
$dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog-min.js') !== FALSE;
$this->assertTrue($dialog_js_exists, 'jQuery UI dialog JS added to the page.');
$dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog.ajax.js') !== FALSE;
$this->assertTrue($dialog_js_exists, 'Drupal dialog JS added to the page.');
// Check that the response matches the expected value.
$this->assertEqual($modal_expected_response, $ajax_result[4], 'POST request modal dialog JSON response matches.');
// Abbreviated test for "normal" dialogs, testing only the difference.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(), 'button2');
$this->assertEqual($normal_expected_response, $ajax_result[4], 'POST request normal dialog JSON response matches.');
// Check that requesting a form dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-form');
// Check we get a chunk of the code, we can't test the whole form as form
// build id and token with be different.
$form = $this->xpath("//form[@id='ajax-test-form']");
$this->assertTrue(!empty($form), 'Non-JS form page present.');
// Emulate going to the JS version of the form and check the JSON response.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-form', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$expected_ajax_settings = [
'edit-preview' => [
'callback' => '::preview',
'event' => 'click',
'url' => Url::fromRoute('ajax_test.dialog_form', [], ['query' => [
MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal',
FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
]])->toString(),
'dialogType' => 'ajax',
'submit' => [
'_triggering_element_name' => 'op',
'_triggering_element_value' => 'Preview',
],
],
];
$this->assertEqual($expected_ajax_settings, $ajax_result[0]['settings']['ajax']);
$this->setRawContent($ajax_result[3]['data']);
// Remove the data, the form build id and token will never match.
unset($ajax_result[3]['data']);
$form = $this->xpath("//form[@id='ajax-test-form']");
$this->assertTrue(!empty($form), 'Modal dialog JSON contains form.');
$this->assertEqual($form_expected_response, $ajax_result[3]);
// Check that requesting an entity form dialog without JS goes to a page.
$this->drupalGet('admin/structure/contact/add');
// Check we get a chunk of the code, we can't test the whole form as form
// build id and token with be different.
$form = $this->xpath("//form[@id='contact-form-add-form']");
$this->assertTrue(!empty($form), 'Non-JS entity form page present.');
// Emulate going to the JS version of the form and check the JSON response.
$ajax_result = $this->drupalGetAjax('admin/structure/contact/add', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$this->setRawContent($ajax_result[3]['data']);
// Remove the data, the form build id and token will never match.
unset($ajax_result[3]['data']);
$form = $this->xpath("//form[@id='contact-form-add-form']");
$this->assertTrue(!empty($form), 'Modal dialog JSON contains entity form.');
$this->assertEqual($entity_form_expected_response, $ajax_result[3]);
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\ElementValidationTest.
*/
namespace Drupal\system\Tests\Ajax;
/**
* Various tests of AJAX behavior.
*
* @group Ajax
*/
class ElementValidationTest extends AjaxTestBase {
/**
* Tries to post an Ajax change to a form that has a validated element.
*
* The drivertext field is Ajax-enabled. An additional field is not, but
* is set to be a required field. In this test the required field is not
* filled in, and we want to see if the activation of the "drivertext"
* Ajax-enabled field fails due to the required field being empty.
*/
function testAjaxElementValidation() {
$edit = array('drivertext' => t('some dumb text'));
// Post with 'drivertext' as the triggering element.
$this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
// Look for a validation failure in the resultant JSON.
$this->assertNoText(t('Error message'), 'No error message in resultant JSON');
$this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked');
$this->drupalGet('ajax_validation_test');
$edit = array('drivernumber' => 12345);
// Post with 'drivernumber' as the triggering element.
$this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivernumber');
// Look for a validation failure in the resultant JSON.
$this->assertNoText(t('Error message'), 'No error message in resultant JSON');
$this->assertText('ajax_forms_test_validation_number_form_callback invoked', 'The correct callback was invoked');
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\FormValuesTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Ajax\DataCommand;
/**
* Tests that form values are properly delivered to AJAX callbacks.
*
* @group Ajax
*/
class FormValuesTest extends AjaxTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(array('access content')));
}
/**
* Submits forms with select and checkbox elements via Ajax.
*/
function testSimpleAjaxFormValue() {
// Verify form values of a select element.
foreach (array('red', 'green', 'blue') as $item) {
$edit = array(
'select' => $item,
);
$commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'select');
$expected = new DataCommand('#ajax_selected_color', 'form_state_value_select', $item);
$this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a selectbox issued with a correct value.');
}
// Verify form values of a checkbox element.
foreach (array(FALSE, TRUE) as $item) {
$edit = array(
'checkbox' => $item,
);
$commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'checkbox');
$expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
$this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
}
// Verify that AJAX elements with invalid callbacks return error code 500.
// Ensure the test error log is empty before these tests.
$this->assertNoErrorsLogged();
foreach (array('null', 'empty', 'nonexistent') as $key) {
$element_name = 'select_' . $key . '_callback';
$edit = array(
$element_name => 'red',
);
$commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name);
$this->assertResponse(500);
}
// The exceptions are expected. Do not interpret them as a test failure.
// Not using File API; a potential error must trigger a PHP warning.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
}
}

View file

@ -0,0 +1,219 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\FrameworkTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Asset\AttachedAssets;
/**
* Performs tests on AJAX framework functions.
*
* @group Ajax
*/
class FrameworkTest extends AjaxTestBase {
/**
* Ensures \Drupal\Core\Ajax\AjaxResponse::ajaxRender() returns JavaScript settings from the page request.
*/
public function testAJAXRender() {
// Verify that settings command is generated if JavaScript settings exist.
$commands = $this->drupalGetAjax('ajax-test/render');
$expected = new SettingsCommand(array('ajax' => 'test'), TRUE);
$this->assertCommand($commands, $expected->render(), '\Drupal\Core\Ajax\AjaxResponse::ajaxRender() loads JavaScript settings.');
}
/**
* Tests AjaxResponse::prepare() AJAX commands ordering.
*/
public function testOrder() {
$expected_commands = array();
// Expected commands, in a very specific order.
$asset_resolver = \Drupal::service('asset.resolver');
$css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
$js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
$renderer = \Drupal::service('renderer');
$expected_commands[0] = new SettingsCommand(array('ajax' => 'test'), TRUE);
$build['#attached']['library'][] = 'ajax_test/order-css-command';
$assets = AttachedAssets::createFromRenderArray($build);
$css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
$expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
$build['#attached']['library'][] = 'ajax_test/order-header-js-command';
$build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
$assets = AttachedAssets::createFromRenderArray($build);
list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
$js_header_render_array = $js_collection_renderer->render($js_assets_header);
$js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
$expected_commands[2] = new PrependCommand('head', $js_header_render_array);
$expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
$expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
// Load any page with at least one CSS file, at least one JavaScript file
// and at least one #ajax-powered element. The latter is an assumption of
// drupalPostAjaxForm(), the two former are assumptions of
// AjaxResponse::ajaxRender().
// @todo refactor AJAX Framework + tests to make less assumptions.
$this->drupalGet('ajax_forms_test_lazy_load_form');
// Verify AJAX command order — this should always be the order:
// 1. JavaScript settings
// 2. CSS files
// 3. JavaScript files in the header
// 4. JavaScript files in the footer
// 5. Any other AJAX commands, in whatever order they were added.
$commands = $this->drupalPostAjaxForm(NULL, array(), NULL, 'ajax-test/order', array(), array(), NULL, array());
$this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.');
$this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).');
$this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.');
$this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.');
$this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.');
}
/**
* Tests the behavior of an error alert command.
*/
public function testAJAXRenderError() {
// Verify custom error message.
$edit = array(
'message' => 'Custom error message.',
);
$commands = $this->drupalGetAjax('ajax-test/render-error', array('query' => $edit));
$expected = new AlertCommand($edit['message']);
$this->assertCommand($commands, $expected->render(), 'Custom error message is output.');
}
/**
* Tests that new JavaScript and CSS files are lazy-loaded on an AJAX request.
*/
public function testLazyLoad() {
$asset_resolver = \Drupal::service('asset.resolver');
$css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
$js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
$renderer = \Drupal::service('renderer');
$expected = array(
'setting_name' => 'ajax_forms_test_lazy_load_form_submit',
'setting_value' => 'executed',
'library_1' => 'system/admin',
'library_2' => 'system/drupal.system',
);
// Get the base page.
$this->drupalGet('ajax_forms_test_lazy_load_form');
$original_settings = $this->getDrupalSettings();
$original_libraries = explode(',', $original_settings['ajaxPageState']['libraries']);
// Verify that the base page doesn't have the settings and files that are to
// be lazy loaded as part of the next requests.
$this->assertTrue(!isset($original_settings[$expected['setting_name']]), format_string('Page originally lacks the %setting, as expected.', array('%setting' => $expected['setting_name'])));
$this->assertTrue(!in_array($expected['library_1'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', array('%library' => $expected['library_1'])));
$this->assertTrue(!in_array($expected['library_2'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', array('%library' => $expected['library_2'])));
// Calculate the expected CSS and JS.
$assets = new AttachedAssets();
$assets->setLibraries([$expected['library_1']])
->setAlreadyLoadedLibraries($original_libraries);
$css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
$expected_css_html = $renderer->renderRoot($css_render_array);
$assets->setLibraries([$expected['library_2']])
->setAlreadyLoadedLibraries($original_libraries);
$js_assets = $asset_resolver->getJsAssets($assets, FALSE)[1];
unset($js_assets['drupalSettings']);
$js_render_array = $js_collection_renderer->render($js_assets);
$expected_js_html = $renderer->renderRoot($js_render_array);
// Submit the AJAX request without triggering files getting added.
$commands = $this->drupalPostAjaxForm(NULL, array('add_files' => FALSE), array('op' => t('Submit')));
$new_settings = $this->getDrupalSettings();
$new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
// Verify the setting was not added when not expected.
$this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', array('%setting' => $expected['setting_name'])));
$this->assertTrue(!in_array($expected['library_1'], $new_libraries), format_string('Page still lacks the %library library, as expected.', array('%library' => $expected['library_1'])));
$this->assertTrue(!in_array($expected['library_2'], $new_libraries), format_string('Page still lacks the %library library, as expected.', array('%library' => $expected['library_2'])));
// Verify a settings command does not add CSS or scripts to drupalSettings
// and no command inserts the corresponding tags on the page.
$found_settings_command = FALSE;
$found_markup_command = FALSE;
foreach ($commands as $command) {
if ($command['command'] == 'settings' && (array_key_exists('css', $command['settings']['ajaxPageState']) || array_key_exists('js', $command['settings']['ajaxPageState']))) {
$found_settings_command = TRUE;
}
if (isset($command['data']) && ($command['data'] == $expected_js_html || $command['data'] == $expected_css_html)) {
$found_markup_command = TRUE;
}
}
$this->assertFalse($found_settings_command, format_string('Page state still lacks the %library_1 and %library_2 libraries, as expected.', array('%library_1' => $expected['library_1'], '%library_2' => $expected['library_2'])));
$this->assertFalse($found_markup_command, format_string('Page still lacks the %library_1 and %library_2 libraries, as expected.', array('%library_1' => $expected['library_1'], '%library_2' => $expected['library_2'])));
// Submit the AJAX request and trigger adding files.
$commands = $this->drupalPostAjaxForm(NULL, array('add_files' => TRUE), array('op' => t('Submit')));
$new_settings = $this->getDrupalSettings();
$new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
// Verify the expected setting was added, both to drupalSettings, and as
// the first AJAX command.
$this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], format_string('Page now has the %setting.', array('%setting' => $expected['setting_name'])));
$expected_command = new SettingsCommand(array($expected['setting_name'] => $expected['setting_value']), TRUE);
$this->assertCommand(array_slice($commands, 0, 1), $expected_command->render(), format_string('The settings command was first.'));
// Verify the expected CSS file was added, both to drupalSettings, and as
// the second AJAX command for inclusion into the HTML.
$this->assertTrue(in_array($expected['library_1'], $new_libraries), format_string('Page state now has the %library library.', array('%library' => $expected['library_1'])));
$this->assertCommand(array_slice($commands, 1, 1), array('data' => $expected_css_html), format_string('Page now has the %library library.', array('%library' => $expected['library_1'])));
// Verify the expected JS file was added, both to drupalSettings, and as
// the third AJAX command for inclusion into the HTML. By testing for an
// exact HTML string containing the SCRIPT tag, we also ensure that
// unexpected JavaScript code, such as a jQuery.extend() that would
// potentially clobber rather than properly merge settings, didn't
// accidentally get added.
$this->assertTrue(in_array($expected['library_2'], $new_libraries), format_string('Page state now has the %library library.', array('%library' => $expected['library_2'])));
$this->assertCommand(array_slice($commands, 2, 1), array('data' => $expected_js_html), format_string('Page now has the %library library.', array('%library' => $expected['library_2'])));
}
/**
* Tests that drupalSettings.currentPath is not updated on AJAX requests.
*/
public function testCurrentPathChange() {
$commands = $this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', array('add_files' => FALSE), array('op' => t('Submit')));
foreach ($commands as $command) {
if ($command['command'] == 'settings') {
$this->assertFalse(isset($command['settings']['currentPath']), 'Value of drupalSettings.currentPath is not updated after an AJAX request.');
}
}
}
/**
* Tests that overridden CSS files are not added during lazy load.
*/
public function testLazyLoadOverriddenCSS() {
// The test theme overrides system.module.css without an implementation,
// thereby removing it.
\Drupal::service('theme_handler')->install(array('test_theme'));
$this->config('system.theme')
->set('default', 'test_theme')
->save();
// This gets the form, and emulates an Ajax submission on it, including
// adding markup to the HEAD and BODY for any lazy loaded JS/CSS files.
$this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', array('add_files' => TRUE), array('op' => t('Submit')));
// Verify that the resulting HTML does not load the overridden CSS file.
// We add a "?" to the assertion, because drupalSettings may include
// information about the file; we only really care about whether it appears
// in a LINK or STYLE tag, for which Drupal always adds a query string for
// cache control.
$this->assertNoText('system.module.css?', 'Ajax lazy loading does not add overridden CSS files.');
}
}

View file

@ -0,0 +1,100 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\MultiFormTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Tests that AJAX-enabled forms work when multiple instances of the same form
* are on a page.
*
* @group Ajax
*/
class MultiFormTest extends AjaxTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('form_test');
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Page'));
// Create a multi-valued field for 'page' nodes to use for Ajax testing.
$field_name = 'field_ajax_test';
entity_create('field_storage_config', array(
'entity_type' => 'node',
'field_name' => $field_name,
'type' => 'text',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
))->save();
entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => 'page',
))->save();
entity_get_form_display('node', 'page', 'default')
->setComponent($field_name, array('type' => 'text_textfield'))
->save();
// Login a user who can create 'page' nodes.
$this->drupalLogin ($this->drupalCreateUser(array('create page content')));
}
/**
* Tests that pages with the 'node_page_form' included twice work correctly.
*/
function testMultiForm() {
// HTML IDs for elements within the field are potentially modified with
// each Ajax submission, but these variables are stable and help target the
// desired elements.
$field_name = 'field_ajax_test';
$form_xpath = '//form[starts-with(@id, "node-page-form")]';
$field_xpath = '//div[contains(@class, "field-name-field-ajax-test")]';
$button_name = $field_name . '_add_more';
$button_value = t('Add another item');
$button_xpath_suffix = '//input[@name="' . $button_name . '"]';
$field_items_xpath_suffix = '//input[@type="text"]';
// Ensure the initial page contains both node forms and the correct number
// of field items and "add more" button for the multi-valued field within
// each form.
$this->drupalGet('form-test/two-instances-of-same-form');
$fields = $this->xpath($form_xpath . $field_xpath);
$this->assertEqual(count($fields), 2);
foreach ($fields as $field) {
$this->assertEqual(count($field->xpath('.' . $field_items_xpath_suffix)), 1, 'Found the correct number of field items on the initial page.');
$this->assertFieldsByValue($field->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button on the initial page.');
}
$this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other');
// Submit the "add more" button of each form twice. After each corresponding
// page update, ensure the same as above.
for ($i = 0; $i < 2; $i++) {
$forms = $this->xpath($form_xpath);
foreach ($forms as $offset => $form) {
$form_html_id = (string) $form['id'];
$this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), NULL, array(), array(), $form_html_id);
$form = $this->xpath($form_xpath)[$offset];
$field = $form->xpath('.' . $field_xpath);
$this->assertEqual(count($field[0]->xpath('.' . $field_items_xpath_suffix)), $i+2, 'Found the correct number of field items after an AJAX submission.');
$this->assertFieldsByValue($field[0]->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button after an AJAX submission.');
$this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');
}
}
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Asset\LibraryDiscoveryIntegrationTest.
*/
namespace Drupal\system\Tests\Asset;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the element info.
*
* @group Render
*/
class LibraryDiscoveryIntegrationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container->get('theme_handler')->install(['test_theme', 'classy']);
}
/**
* Ensures that the element info can be altered by themes.
*/
public function testElementInfoByTheme() {
/** @var \Drupal\Core\Theme\ThemeInitializationInterface $theme_initializer */
$theme_initializer = $this->container->get('theme.initialization');
/** @var \Drupal\Core\Theme\ThemeManagerInterface $theme_manager */
$theme_manager = $this->container->get('theme.manager');
/** @var \Drupal\Core\Render\ElementInfoManagerInterface $element_info */
$library_discovery = $this->container->get('library.discovery');
$theme_manager->setActiveTheme($theme_initializer->getActiveThemeByName('test_theme'));
$this->assertTrue($library_discovery->getLibraryByName('test_theme', 'kitten'));
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Batch\PageTest.
*/
namespace Drupal\system\Tests\Batch;
use Drupal\simpletest\WebTestBase;
/**
* Tests the content of the progress page.
*
* @group Batch
*/
class PageTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('batch_test');
/**
* Tests that the batch API progress page uses the correct theme.
*/
function testBatchProgressPageTheme() {
// Make sure that the page which starts the batch (an administrative page)
// is using a different theme than would normally be used by the batch API.
$this->container->get('theme_handler')->install(array('seven', 'bartik'));
$this->config('system.theme')
->set('default', 'bartik')
->set('admin', 'seven')
->save();
// Log in as an administrator who can see the administrative theme.
$admin_user = $this->drupalCreateUser(array('view the administration theme'));
$this->drupalLogin($admin_user);
// Visit an administrative page that runs a test batch, and check that the
// theme that was used during batch execution (which the batch callback
// function saved as a variable) matches the theme used on the
// administrative page.
$this->drupalGet('admin/batch-test/test-theme');
// The stack should contain the name of the theme used on the progress
// page.
$this->assertEqual(batch_test_stack(), array('seven'), 'A progressive batch correctly uses the theme of the page that started the batch.');
}
/**
* Tests that the batch API progress page shows the title correctly.
*/
function testBatchProgressPageTitle() {
// Visit an administrative page that runs a test batch, and check that the
// title shown during batch execution (which the batch callback function
// saved as a variable) matches the theme used on the administrative page.
$this->drupalGet('batch-test/test-title');
// The stack should contain the title shown on the progress page.
$this->assertEqual(batch_test_stack(), ['Batch Test'], 'The batch title is shown on the batch page.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
}

View file

@ -0,0 +1,279 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Batch\ProcessingTest.
*/
namespace Drupal\system\Tests\Batch;
use Drupal\simpletest\WebTestBase;
/**
* Tests batch processing in form and non-form workflow.
*
* @group Batch
*/
class ProcessingTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('batch_test');
/**
* Tests batches triggered outside of form submission.
*/
function testBatchNoForm() {
// Displaying the page triggers batch 1.
$this->drupalGet('batch-test/no-form');
$this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch for step 2 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in a form submit handler.
*/
function testBatchForm() {
// Batch 0: no operation.
$edit = array('batch' => 'batch_0');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_0'), 'Batch with no operation performed successfully.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 1: several simple operations.
$edit = array('batch' => 'batch_1');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch with simple operations performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 2: one multistep operation.
$edit = array('batch' => 'batch_2');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_2'), 'Batch with multistep operation performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_2'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 3: simple + multistep combined.
$edit = array('batch' => 'batch_3');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_3'), 'Batch with simple and multistep operations performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_3'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 4: nested batch.
$edit = array('batch' => 'batch_4');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_4'), 'Nested batch performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_4'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in a multistep form.
*/
function testBatchFormMultistep() {
$this->drupalGet('batch-test/multistep');
$this->assertText('step 1', 'Form is displayed in step 1.');
// First step triggers batch 1.
$this->drupalPostForm(NULL, array(), 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch for step 1 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
$this->assertText('step 2', 'Form is displayed in step 2.');
// Second step triggers batch 2.
$this->drupalPostForm(NULL, array(), 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_2'), 'Batch for step 2 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_2'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in different submit handlers on the same form.
*/
function testBatchFormMultipleBatches() {
// Batches 1, 2 and 3 are triggered in sequence by different submit
// handlers. Each submit handler modify the submitted 'value'.
$value = rand(0, 255);
$edit = array('value' => $value);
$this->drupalPostForm('batch-test/chained', $edit, 'Submit');
// Check that result messages are present and in the correct order.
$this->assertBatchMessages($this->_resultMessages('chained'), 'Batches defined in separate submit handlers performed successfully.');
// The stack contains execution order of batch callbacks and submit
// handlers and logging of corresponding $form_state->getValues().
$this->assertEqual(batch_test_stack(), $this->_resultStack('chained', $value), 'Execution order was correct, and $form_state is correctly persisted.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in a programmatically submitted form.
*
* Same as above, but the form is submitted through drupal_form_execute().
*/
function testBatchFormProgrammatic() {
// Batches 1, 2 and 3 are triggered in sequence by different submit
// handlers. Each submit handler modify the submitted 'value'.
$value = rand(0, 255);
$this->drupalGet('batch-test/programmatic/' . $value);
// Check that result messages are present and in the correct order.
$this->assertBatchMessages($this->_resultMessages('chained'), 'Batches defined in separate submit handlers performed successfully.');
// The stack contains execution order of batch callbacks and submit
// handlers and logging of corresponding $form_state->getValues().
$this->assertEqual(batch_test_stack(), $this->_resultStack('chained', $value), 'Execution order was correct, and $form_state is correctly persisted.');
$this->assertText('Got out of a programmatic batched form.', 'Page execution continues normally.');
}
/**
* Test form submission during a batch operation.
*/
function testDrupalFormSubmitInBatch() {
// Displaying the page triggers a batch that programmatically submits a
// form.
$value = rand(0, 255);
$this->drupalGet('batch-test/nested-programmatic/' . $value);
$this->assertEqual(batch_test_stack(), array('mock form submitted with value = ' . $value), '\Drupal::formBuilder()->submitForm() ran successfully within a batch operation.');
}
/**
* Tests batches that return $context['finished'] > 1 do in fact complete.
*
* @see https://www.drupal.org/node/600836
*/
function testBatchLargePercentage() {
// Displaying the page triggers batch 5.
$this->drupalGet('batch-test/large-percentage');
$this->assertBatchMessages($this->_resultMessages('batch_5'), 'Batch for step 2 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_5'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Triggers a pass if the texts were found in order in the raw content.
*
* @param $texts
* Array of raw strings to look for .
* @param $message
* Message to display.
*
* @return
* TRUE on pass, FALSE on fail.
*/
function assertBatchMessages($texts, $message) {
$pattern = '|' . implode('.*', $texts) .'|s';
return $this->assertPattern($pattern, $message);
}
/**
* Returns expected execution stacks for the test batches.
*/
function _resultStack($id, $value = 0) {
$stack = array();
switch ($id) {
case 'batch_1':
for ($i = 1; $i <= 10; $i++) {
$stack[] = "op 1 id $i";
}
break;
case 'batch_2':
for ($i = 1; $i <= 10; $i++) {
$stack[] = "op 2 id $i";
}
break;
case 'batch_3':
for ($i = 1; $i <= 5; $i++) {
$stack[] = "op 1 id $i";
}
for ($i = 1; $i <= 5; $i++) {
$stack[] = "op 2 id $i";
}
for ($i = 6; $i <= 10; $i++) {
$stack[] = "op 1 id $i";
}
for ($i = 6; $i <= 10; $i++) {
$stack[] = "op 2 id $i";
}
break;
case 'batch_4':
for ($i = 1; $i <= 5; $i++) {
$stack[] = "op 1 id $i";
}
$stack[] = 'setting up batch 2';
for ($i = 6; $i <= 10; $i++) {
$stack[] = "op 1 id $i";
}
$stack = array_merge($stack, $this->_resultStack('batch_2'));
break;
case 'batch_5':
for ($i = 1; $i <= 10; $i++) {
$stack[] = "op 5 id $i";
}
break;
case 'chained':
$stack[] = 'submit handler 1';
$stack[] = 'value = ' . $value;
$stack = array_merge($stack, $this->_resultStack('batch_1'));
$stack[] = 'submit handler 2';
$stack[] = 'value = ' . ($value + 1);
$stack = array_merge($stack, $this->_resultStack('batch_2'));
$stack[] = 'submit handler 3';
$stack[] = 'value = ' . ($value + 2);
$stack[] = 'submit handler 4';
$stack[] = 'value = ' . ($value + 3);
$stack = array_merge($stack, $this->_resultStack('batch_3'));
break;
}
return $stack;
}
/**
* Returns expected result messages for the test batches.
*/
function _resultMessages($id) {
$messages = array();
switch ($id) {
case 'batch_0':
$messages[] = 'results for batch 0<br>none';
break;
case 'batch_1':
$messages[] = 'results for batch 1<br>op 1: processed 10 elements';
break;
case 'batch_2':
$messages[] = 'results for batch 2<br>op 2: processed 10 elements';
break;
case 'batch_3':
$messages[] = 'results for batch 3<br>op 1: processed 10 elements<br>op 2: processed 10 elements';
break;
case 'batch_4':
$messages[] = 'results for batch 4<br>op 1: processed 10 elements';
$messages = array_merge($messages, $this->_resultMessages('batch_2'));
break;
case 'batch_5':
$messages[] = 'results for batch 5<br>op 5: processed 10 elements';
break;
case 'chained':
$messages = array_merge($messages, $this->_resultMessages('batch_1'));
$messages = array_merge($messages, $this->_resultMessages('batch_2'));
$messages = array_merge($messages, $this->_resultMessages('batch_3'));
break;
}
return $messages;
}
}

View file

@ -0,0 +1,314 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Block\SystemMenuBlockTest.
*/
namespace Drupal\system\Tests\Block;
use Drupal\Core\Render\Element;
use Drupal\simpletest\KernelTestBase;
use Drupal\system\Tests\Routing\MockRouteProvider;
use Drupal\Tests\Core\Menu\MenuLinkMock;
use Drupal\user\Entity\User;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Tests \Drupal\system\Plugin\Block\SystemMenuBlock.
*
* @group Block
* @todo Expand test coverage to all SystemMenuBlock functionality, including
* block_menu_delete().
*
* @see \Drupal\system\Plugin\Derivative\SystemMenuBlock
* @see \Drupal\system\Plugin\Block\SystemMenuBlock
*/
class SystemMenuBlockTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'system',
'block',
'menu_test',
'menu_link_content',
'field',
'user',
'link',
);
/**
* The block under test.
*
* @var \Drupal\system\Plugin\Block\SystemMenuBlock
*/
protected $block;
/**
* The menu for testing.
*
* @var \Drupal\system\MenuInterface
*/
protected $menu;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTree
*/
protected $linkTree;
/**
* The menu link plugin manager service.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
*/
protected $menuLinkManager;
/**
* The block manager service.
*
* @var \Drupal\Core\block\BlockManagerInterface
*/
protected $blockManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'sequences');
$this->installEntitySchema('user');
$this->installSchema('system', array('router'));
$this->installEntitySchema('menu_link_content');
$account = User::create([
'name' => $this->randomMachineName(),
'status' => 1,
]);
$account->save();
$this->container->get('current_user')->setAccount($account);
$this->menuLinkManager = $this->container->get('plugin.manager.menu.link');
$this->linkTree = $this->container->get('menu.link_tree');
$this->blockManager = $this->container->get('plugin.manager.block');
$routes = new RouteCollection();
$requirements = array('_access' => 'TRUE');
$options = array('_access_checks' => array('access_check.default'));
$routes->add('example1', new Route('/example1', array(), $requirements, $options));
$routes->add('example2', new Route('/example2', array(), $requirements, $options));
$routes->add('example3', new Route('/example3', array(), $requirements, $options));
$routes->add('example4', new Route('/example4', array(), $requirements, $options));
$routes->add('example5', new Route('/example5', array(), $requirements, $options));
$routes->add('example6', new Route('/example6', array(), $requirements, $options));
$routes->add('example7', new Route('/example7', array(), $requirements, $options));
$routes->add('example8', new Route('/example8', array(), $requirements, $options));
$mock_route_provider = new MockRouteProvider($routes);
$this->container->set('router.route_provider', $mock_route_provider);
// Add a new custom menu.
$menu_name = 'mock';
$label = $this->randomMachineName(16);
$this->menu = entity_create('menu', array(
'id' => $menu_name,
'label' => $label,
'description' => 'Description text',
));
$this->menu->save();
// This creates a tree with the following structure:
// - 1
// - 2
// - 3
// - 4
// - 5
// - 7
// - 6
// - 8
// With link 6 being the only external link.
$links = array(
1 => MenuLinkMock::create(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '', 'weight' => 0)),
2 => MenuLinkMock::create(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => '', 'route_parameters' => array('foo' => 'bar'), 'weight' => 1)),
3 => MenuLinkMock::create(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'weight' => 2)),
4 => MenuLinkMock::create(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3', 'weight' => 3)),
5 => MenuLinkMock::create(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '', 'expanded' => TRUE, 'weight' => 4)),
6 => MenuLinkMock::create(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'barbar', 'parent' => '', 'weight' => 5)),
7 => MenuLinkMock::create(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => 'test.example5', 'weight' => 6)),
8 => MenuLinkMock::create(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '', 'weight' => 7)),
);
foreach ($links as $instance) {
$this->menuLinkManager->addDefinition($instance->getPluginId(), $instance->getPluginDefinition());
}
}
/**
* Tests calculation of a system menu block's configuration dependencies.
*/
public function testSystemMenuBlockConfigDependencies() {
$block = entity_create('block', array(
'plugin' => 'system_menu_block:' . $this->menu->id(),
'region' => 'footer',
'id' => 'machinename',
'theme' => 'stark',
));
$dependencies = $block->calculateDependencies();
$expected = array(
'config' => array(
'system.menu.' . $this->menu->id()
),
'module' => array(
'system'
),
'theme' => array(
'stark'
),
);
$this->assertIdentical($expected, $dependencies);
}
/**
* Tests the config start level and depth.
*/
public function testConfigLevelDepth() {
// Helper function to generate a configured block instance.
$place_block = function ($level, $depth) {
return $this->blockManager->createInstance('system_menu_block:' . $this->menu->id(), array(
'region' => 'footer',
'id' => 'machinename',
'theme' => 'stark',
'level' => $level,
'depth' => $depth,
));
};
// All the different block instances we're going to test.
$blocks = [
'all' => $place_block(1, 0),
'level_1_only' => $place_block(1, 1),
'level_2_only' => $place_block(2, 1),
'level_3_only' => $place_block(3, 1),
'level_1_and_beyond' => $place_block(1, 0),
'level_2_and_beyond' => $place_block(2, 0),
'level_3_and_beyond' => $place_block(3, 0),
];
// Scenario 1: test all block instances when there's no active trail.
$no_active_trail_expectations = [];
$no_active_trail_expectations['all'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [
'test.example7' => [],
],
'test.example6' => [],
'test.example8' => [],
];
$no_active_trail_expectations['level_1_only'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [],
'test.example6' => [],
'test.example8' => [],
];
$no_active_trail_expectations['level_2_only'] = [
'test.example7' => [],
];
$no_active_trail_expectations['level_3_only'] = [];
$no_active_trail_expectations['level_1_and_beyond'] = $no_active_trail_expectations['all'];
$no_active_trail_expectations['level_2_and_beyond'] = $no_active_trail_expectations['level_2_only'];
$no_active_trail_expectations['level_3_and_beyond'] = [];
foreach ($blocks as $id => $block) {
$block_build = $block->build();
$items = isset($block_build['#items']) ? $block_build['#items'] : [];
$this->assertIdentical($no_active_trail_expectations[$id], $this->convertBuiltMenuToIdTree($items), format_string('Menu block %id with no active trail renders the expected tree.', ['%id' => $id]));
}
// Scenario 2: test all block instances when there's an active trail.
$route = $this->container->get('router.route_provider')->getRouteByName('example3');
$request = new Request();
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'example3');
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route);
$this->container->get('request_stack')->push($request);
// \Drupal\Core\Menu\MenuActiveTrail uses the cache collector pattern, which
// includes static caching. Since this second scenario simulates a second
// request, we must also simulate it for the MenuActiveTrail service, by
// clearing the cache collector's static cache.
\Drupal::service('menu.active_trail')->clear();
$active_trail_expectations = [];
$active_trail_expectations['all'] = [
'test.example1' => [],
'test.example2' => [
'test.example3' => [
'test.example4' => [],
]
],
'test.example5' => [
'test.example7' => [],
],
'test.example6' => [],
'test.example8' => [],
];
$active_trail_expectations['level_1_only'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [],
'test.example6' => [],
'test.example8' => [],
];
$active_trail_expectations['level_2_only'] = [
'test.example3' => [],
'test.example7' => [],
];
$active_trail_expectations['level_3_only'] = [
'test.example4' => [],
];
$active_trail_expectations['level_1_and_beyond'] = $active_trail_expectations['all'];
$active_trail_expectations['level_2_and_beyond'] = [
'test.example3' => [
'test.example4' => [],
],
'test.example7' => [],
];
$active_trail_expectations['level_3_and_beyond'] = $active_trail_expectations['level_3_only'];
foreach ($blocks as $id => $block) {
$block_build = $block->build();
$items = isset($block_build['#items']) ? $block_build['#items'] : [];
$this->assertIdentical($active_trail_expectations[$id], $this->convertBuiltMenuToIdTree($items), format_string('Menu block %id with an active trail renders the expected tree.', ['%id' => $id]));
}
}
/**
* Helper method to allow for easy menu link tree structure assertions.
*
* Converts the result of MenuLinkTree::build() in a "menu link ID tree".
*
* @param array $build
* The return value of of MenuLinkTree::build()
*
* @return array
* The "menu link ID tree" representation of the given render array.
*/
protected function convertBuiltMenuToIdTree(array $build) {
$level = [];
foreach (Element::children($build) as $id) {
$level[$id] = [];
if (isset($build[$id]['below'])) {
$level[$id] = $this->convertBuiltMenuToIdTree($build[$id]['below']);
}
}
return $level;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Bootstrap\DrupalSetMessageTest.
*/
namespace Drupal\system\Tests\Bootstrap;
use Drupal\simpletest\WebTestBase;
/**
* Tests drupal_set_message() and related functions.
*
* @group Bootstrap
*/
class DrupalSetMessageTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system_test');
/**
* Tests setting messages and removing one before it is displayed.
*/
function testSetRemoveMessages() {
// The page at system-test/drupal-set-message sets two messages and then
// removes the first before it is displayed.
$this->drupalGet('system-test/drupal-set-message');
$this->assertNoText('First message (removed).');
$this->assertText('Second message (not removed).');
}
/**
* Tests setting duplicated messages.
*/
function testDuplicatedMessages() {
$this->drupalGet('system-test/drupal-set-message');
$this->assertUniqueText('Non Duplicated message');
$this->assertNoUniqueText('Duplicated message');
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Bootstrap\GetFilenameUnitTest.
*/
namespace Drupal\system\Tests\Bootstrap;
use Drupal\simpletest\KernelTestBase;
/**
* Tests that drupal_get_filename() works correctly.
*
* @group Bootstrap
*/
class GetFilenameUnitTest extends KernelTestBase {
/**
* Tests that drupal_get_filename() works when the file is not in database.
*/
function testDrupalGetFilename() {
// drupal_get_profile() is using obtaining the profile from state if the
// install_state global is not set.
global $install_state;
$install_state['parameters']['profile'] = 'testing';
// Rebuild system.module.files state data.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_static_reset('system_rebuild_module_data');
system_rebuild_module_data();
// Retrieving the location of a module.
$this->assertIdentical(drupal_get_filename('module', 'system'), 'core/modules/system/system.info.yml');
// Retrieving the location of a theme.
\Drupal::service('theme_handler')->install(array('stark'));
$this->assertIdentical(drupal_get_filename('theme', 'stark'), 'core/themes/stark/stark.info.yml');
// Retrieving the location of a theme engine.
$this->assertIdentical(drupal_get_filename('theme_engine', 'twig'), 'core/themes/engines/twig/twig.info.yml');
// Retrieving the location of a profile. Profiles are a special case with
// a fixed location and naming.
$this->assertIdentical(drupal_get_filename('profile', 'testing'), 'core/profiles/testing/testing.info.yml');
// Generate a non-existing module name.
$non_existing_module = uniqid("", TRUE);
// Set a custom error handler so we can ignore the file not found error.
set_error_handler(function($severity, $message, $file, $line) {
// Skip error handling if this is a "file not found" error.
if (strstr($message, 'is missing from the file system:')) {
\Drupal::state()->set('get_filename_test_triggered_error', TRUE);
return;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for an item that does not exist returns NULL.');
$this->assertTrue(\Drupal::state()->get('get_filename_test_triggered_error'), 'Searching for an item that does not exist triggers an error.');
// Restore the original error handler.
restore_error_handler();
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Bootstrap\ResettableStaticUnitTest.
*/
namespace Drupal\system\Tests\Bootstrap;
use Drupal\simpletest\KernelTestBase;
/**
* Tests that drupal_static() and drupal_static_reset() work.
*
* @group Bootstrap
*/
class ResettableStaticUnitTest extends KernelTestBase {
/**
* Tests drupal_static() function.
*
* Tests that a variable reference returned by drupal_static() gets reset when
* drupal_static_reset() is called.
*/
function testDrupalStatic() {
$name = __CLASS__ . '_' . __METHOD__;
$var = &drupal_static($name, 'foo');
$this->assertEqual($var, 'foo', 'Variable returned by drupal_static() was set to its default.');
// Call the specific reset and the global reset each twice to ensure that
// multiple resets can be issued without odd side effects.
$var = 'bar';
drupal_static_reset($name);
$this->assertEqual($var, 'foo', 'Variable was reset after first invocation of name-specific reset.');
$var = 'bar';
drupal_static_reset($name);
$this->assertEqual($var, 'foo', 'Variable was reset after second invocation of name-specific reset.');
$var = 'bar';
drupal_static_reset();
$this->assertEqual($var, 'foo', 'Variable was reset after first invocation of global reset.');
$var = 'bar';
drupal_static_reset();
$this->assertEqual($var, 'foo', 'Variable was reset after second invocation of global reset.');
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\ApcuBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\ApcuBackend;
/**
* Tests the APCu cache backend.
*
* @group Cache
* @requires extension apc
*/
class ApcuBackendUnitTest extends GenericCacheBackendUnitTestBase {
protected function checkRequirements() {
$requirements = parent::checkRequirements();
if (!extension_loaded('apc')) {
$requirements[] = 'APC extension not found.';
}
else {
if (version_compare(phpversion('apc'), '3.1.1', '<')) {
$requirements[] = 'APC extension must be newer than 3.1.1 for APCIterator support.';
}
if (PHP_SAPI === 'cli' && !ini_get('apc.enable_cli')) {
$requirements[] = 'apc.enable_cli must be enabled to run this test.';
}
}
return $requirements;
}
protected function createCacheBackend($bin) {
return new ApcuBackend($bin, $this->databasePrefix, \Drupal::service('cache_tags.invalidator.checksum'));
}
protected function tearDown() {
foreach ($this->cachebackends as $bin => $cachebackend) {
$this->cachebackends[$bin]->removeBin();
}
parent::tearDown();
}
}

View file

@ -0,0 +1,147 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Url;
/**
* Provides test assertions for testing page-level cache contexts & tags.
*
* Can be used by test classes that extend \Drupal\simpletest\WebTestBase.
*/
trait AssertPageCacheContextsAndTagsTrait {
/**
* Enables page caching.
*/
protected function enablePageCaching() {
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Gets a specific header value as array.
*
* @param string $header_name
* The header name.
*
* @return string[]
* The header value, potentially exploded by spaces.
*/
protected function getCacheHeaderValues($header_name) {
$header_value = $this->drupalGetHeader($header_name);
if (empty($header_value)) {
return [];
}
else {
return explode(' ', $header_value);
}
}
/**
* Asserts page cache miss, then hit for the given URL; checks cache headers.
*
* @param \Drupal\Core\Url $url
* The URL to test.
* @param string[] $expected_contexts
* The expected cache contexts for the given URL.
* @param string[] $expected_tags
* The expected cache tags for the given URL.
*/
protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
$absolute_url = $url->setAbsolute()->toString();
sort($expected_contexts);
sort($expected_tags);
// Assert cache miss + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
$this->assertCacheTags($expected_tags);
$this->assertCacheContexts($expected_contexts);
// Assert cache hit + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertCacheTags($expected_tags);
$this->assertCacheContexts($expected_contexts);
// Assert page cache item + expected cache tags.
$cid_parts = array($url->setAbsolute()->toString(), 'html');
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('render')->get($cid);
sort($cache_entry->tags);
$this->assertEqual($cache_entry->tags, $expected_tags);
$this->debugCacheTags($cache_entry->tags, $expected_tags);
}
/**
* Provides debug information for cache tags.
*
* @param string[] $actual_tags
* The actual cache tags.
* @param string[] $expected_tags
* The expected cache tags.
*/
protected function debugCacheTags(array $actual_tags, array $expected_tags) {
if ($actual_tags !== $expected_tags) {
debug('Missing cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
debug('Unwanted cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
}
}
/**
* Ensures that some cache tags are present in the current response.
*
* @param string[] $expected_tags
* The expected tags.
*/
protected function assertCacheTags(array $expected_tags) {
$actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
sort($expected_tags);
sort($actual_tags);
$this->assertIdentical($actual_tags, $expected_tags);
$this->debugCacheTags($actual_tags, $expected_tags);
}
/**
* Ensures that some cache contexts are present in the current response.
*
* @param string[] $expected_contexts
* The expected cache contexts.
* @param string $message
* (optional) A verbose message to output.
*
* @return
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertCacheContexts(array $expected_contexts, $message = NULL) {
$actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts');
sort($expected_contexts);
sort($actual_contexts);
$return = $this->assertIdentical($actual_contexts, $expected_contexts, $message);
if (!$return) {
debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
}
return $return;
}
/**
* Asserts the max age header.
*
* @param int $max_age
*/
protected function assertCacheMaxAge($max_age) {
$cache_control_header = $this->drupalGetHeader('Cache-Control');
if (strpos($cache_control_header, 'max-age:' . $max_age) === FALSE) {
debug('Expected max-age:' . $max_age . '; Response max-age:' . $cache_control_header);
}
$this->assertTrue(strpos($cache_control_header, 'max-age:' . $max_age));
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\BackendChainUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\BackendChain;
use Drupal\Core\Cache\MemoryBackend;
/**
* Unit test of the backend chain using the generic cache unit test base.
*
* @group Cache
*/
class BackendChainUnitTest extends GenericCacheBackendUnitTestBase {
protected function createCacheBackend($bin) {
$chain = new BackendChain($bin);
// We need to create some various backends in the chain.
$chain
->appendBackend(new MemoryBackend('foo'))
->prependBackend(new MemoryBackend('bar'))
->appendBackend(new MemoryBackend('baz'));
\Drupal::service('cache_tags.invalidator')->addInvalidator($chain);
return $chain;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\CacheTestBase.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\simpletest\WebTestBase;
/**
* Provides helper methods for cache tests.
*/
abstract class CacheTestBase extends WebTestBase {
protected $defaultBin = 'render';
protected $defaultCid = 'test_temporary';
protected $defaultValue = 'CacheTest';
/**
* Checks whether or not a cache entry exists.
*
* @param $cid
* The cache id.
* @param $var
* The variable the cache should contain.
* @param $bin
* The bin the cache item was stored in.
* @return
* TRUE on pass, FALSE on fail.
*/
protected function checkCacheExists($cid, $var, $bin = NULL) {
if ($bin == NULL) {
$bin = $this->defaultBin;
}
$cached = \Drupal::cache($bin)->get($cid);
return isset($cached->data) && $cached->data == $var;
}
/**
* Asserts that a cache entry exists.
*
* @param $message
* Message to display.
* @param $var
* The variable the cache should contain.
* @param $cid
* The cache id.
* @param $bin
* The bin the cache item was stored in.
*/
protected function assertCacheExists($message, $var = NULL, $cid = NULL, $bin = NULL) {
if ($bin == NULL) {
$bin = $this->defaultBin;
}
if ($cid == NULL) {
$cid = $this->defaultCid;
}
if ($var == NULL) {
$var = $this->defaultValue;
}
$this->assertTrue($this->checkCacheExists($cid, $var, $bin), $message);
}
/**
* Asserts that a cache entry has been removed.
*
* @param $message
* Message to display.
* @param $cid
* The cache id.
* @param $bin
* The bin the cache item was stored in.
*/
function assertCacheRemoved($message, $cid = NULL, $bin = NULL) {
if ($bin == NULL) {
$bin = $this->defaultBin;
}
if ($cid == NULL) {
$cid = $this->defaultCid;
}
$cached = \Drupal::cache($bin)->get($cid);
$this->assertFalse($cached, $message);
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\ChainedFastBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\ChainedFastBackend;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\Core\Cache\PhpBackend;
/**
* Unit test of the fast chained backend using the generic cache unit test base.
*
* @group Cache
*/
class ChainedFastBackendUnitTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of ChainedFastBackend.
*
* @return \Drupal\Core\Cache\ChainedFastBackend
* A new ChainedFastBackend object.
*/
protected function createCacheBackend($bin) {
$consistent_backend = new DatabaseBackend(\Drupal::service('database'), \Drupal::service('cache_tags.invalidator.checksum'), $bin);
$fast_backend = new PhpBackend($bin, \Drupal::service('cache_tags.invalidator.checksum'));
$backend = new ChainedFastBackend($consistent_backend, $fast_backend, $bin);
// Explicitly register the cache bin as it can not work through the
// cache bin list in the container.
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\ClearTest.
*/
namespace Drupal\system\Tests\Cache;
/**
* Tests our clearing is done the proper way.
*
* @group Cache
*/
use Drupal\Core\Cache\Cache;
class ClearTest extends CacheTestBase {
protected function setUp() {
$this->defaultBin = 'render';
$this->defaultValue = $this->randomMachineName(10);
parent::setUp();
}
/**
* Tests drupal_flush_all_caches().
*/
function testFlushAllCaches() {
// Create cache entries for each flushed cache bin.
$bins = Cache::getBins();
$this->assertTrue($bins, 'Cache::getBins() returned bins to flush.');
foreach ($bins as $bin => $cache_backend) {
$cid = 'test_cid_clear' . $bin;
$cache_backend->set($cid, $this->defaultValue);
}
// Remove all caches then make sure that they are cleared.
drupal_flush_all_caches();
foreach ($bins as $bin => $cache_backend) {
$cid = 'test_cid_clear' . $bin;
$this->assertFalse($this->checkCacheExists($cid, $this->defaultValue, $bin), format_string('All cache entries removed from @bin.', array('@bin' => $bin)));
}
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\DatabaseBackendTagTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Tests DatabaseBackend cache tag implementation.
*
* @group Cache
*/
class DatabaseBackendTagTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system');
/**
* {@inheritdoc}
*/
public function containerBuild(ContainerBuilder $container) {
parent::containerBuild($container);
// Change container to database cache backends.
$container
->register('cache_factory', 'Drupal\Core\Cache\CacheFactory')
->addArgument(new Reference('settings'))
->addMethodCall('setContainer', array(new Reference('service_container')));
}
public function testTagInvalidations() {
// Create cache entry in multiple bins.
$tags = array('test_tag:1', 'test_tag:2', 'test_tag:3');
$bins = array('data', 'bootstrap', 'render');
foreach ($bins as $bin) {
$bin = \Drupal::cache($bin);
$bin->set('test', 'value', Cache::PERMANENT, $tags);
$this->assertTrue($bin->get('test'), 'Cache item was set in bin.');
}
$invalidations_before = intval(db_select('cachetags')->fields('cachetags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField());
Cache::invalidateTags(array('test_tag:2'));
// Test that cache entry has been invalidated in multiple bins.
foreach ($bins as $bin) {
$bin = \Drupal::cache($bin);
$this->assertFalse($bin->get('test'), 'Tag invalidation affected item in bin.');
}
// Test that only one tag invalidation has occurred.
$invalidations_after = intval(db_select('cachetags')->fields('cachetags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField());
$this->assertEqual($invalidations_after, $invalidations_before + 1, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.');
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\DatabaseBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\DatabaseBackend;
/**
* Unit test of the database backend using the generic cache unit test base.
*
* @group Cache
*/
class DatabaseBackendUnitTest extends GenericCacheBackendUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system');
/**
* Creates a new instance of DatabaseBackend.
*
* @return
* A new DatabaseBackend object.
*/
protected function createCacheBackend($bin) {
return new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin);
}
/**
* {@inheritdoc}
*/
public function testSetGet() {
parent::testSetGet();
$backend = $this->getCacheBackend();
// Set up a cache ID that is not ASCII and longer than 255 characters so we
// can test cache ID normalization.
$cid_long = str_repeat('愛€', 500);
$cached_value_long = $this->randomMachineName();
$backend->set($cid_long, $cached_value_long);
$this->assertIdentical($cached_value_long, $backend->get($cid_long)->data, "Backend contains the correct value for long, non-ASCII cache id.");
$cid_short = '愛1€';
$cached_value_short = $this->randomMachineName();
$backend->set($cid_short, $cached_value_short);
$this->assertIdentical($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id.");
}
}

View file

@ -0,0 +1,624 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\simpletest\KernelTestBase;
/**
* Tests any cache backend.
*
* Full generic unit test suite for any cache backend. In order to use it for a
* cache backend implementation, extend this class and override the
* createBackendInstance() method to return an object.
*
* @see DatabaseBackendUnitTestCase
* For a full working implementation.
*/
abstract class GenericCacheBackendUnitTestBase extends KernelTestBase {
/**
* Array of objects implementing Drupal\Core\Cache\CacheBackendInterface.
*
* @var array
*/
protected $cachebackends;
/**
* Cache bin to use for testing.
*
* @var string
*/
protected $testBin;
/**
* Random value to use in tests.
*
* @var string
*/
protected $defaultValue;
/**
* Gets the testing bin.
*
* Override this method if you want to work on a different bin than the
* default one.
*
* @return string
* Bin name.
*/
protected function getTestBin() {
if (!isset($this->testBin)) {
$this->testBin = 'page';
}
return $this->testBin;
}
/**
* Creates a cache backend to test.
*
* Override this method to test a CacheBackend.
*
* @param string $bin
* Bin name to use for this backend instance.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
protected abstract function createCacheBackend($bin);
/**
* Allows specific implementation to change the environment before a test run.
*/
public function setUpCacheBackend() {
}
/**
* Allows alteration of environment after a test run but before tear down.
*
* Used before the real tear down because the tear down will change things
* such as the database prefix.
*/
public function tearDownCacheBackend() {
}
/**
* Gets a backend to test; this will get a shared instance set in the object.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
protected function getCacheBackend($bin = null) {
if (!isset($bin)) {
$bin = $this->getTestBin();
}
if (!isset($this->cachebackends[$bin])) {
$this->cachebackends[$bin] = $this->createCacheBackend($bin);
// Ensure the backend is empty.
$this->cachebackends[$bin]->deleteAll();
}
return $this->cachebackends[$bin];
}
protected function setUp() {
$this->cachebackends = array();
$this->defaultValue = $this->randomMachineName(10);
parent::setUp();
$this->setUpCacheBackend();
}
protected function tearDown() {
// Destruct the registered backend, each test will get a fresh instance,
// properly emptying it here ensure that on persistent data backends they
// will come up empty the next test.
foreach ($this->cachebackends as $bin => $cachebackend) {
$this->cachebackends[$bin]->deleteAll();
}
unset($this->cachebackends);
$this->tearDownCacheBackend();
parent::tearDown();
}
/**
* Tests the get and set methods of Drupal\Core\Cache\CacheBackendInterface.
*/
public function testSetGet() {
$backend = $this->getCacheBackend();
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1.");
$with_backslash = array('foo' => '\Drupal\foo\Bar');
$backend->set('test1', $with_backslash);
$cached = $backend->get('test1');
$this->assert(is_object($cached), "Backend returned an object for cache id test1.");
$this->assertIdentical($with_backslash, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
// We need to round because microtime may be rounded up in the backend.
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.');
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2.");
$backend->set('test2', array('value' => 3), REQUEST_TIME + 3);
$cached = $backend->get('test2');
$this->assert(is_object($cached), "Backend returned an object for cache id test2.");
$this->assertIdentical(array('value' => 3), $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, REQUEST_TIME + 3, 'Expire time is correct.');
$backend->set('test3', 'foobar', REQUEST_TIME - 3);
$this->assertFalse($backend->get('test3'), 'Invalid item not returned.');
$cached = $backend->get('test3', TRUE);
$this->assert(is_object($cached), 'Backend returned an object for cache id test3.');
$this->assertFalse($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, REQUEST_TIME - 3, 'Expire time is correct.');
$this->assertIdentical(FALSE, $backend->get('test4'), "Backend does not contain data for cache id test4.");
$with_eof = array('foo' => "\nEOF\ndata");
$backend->set('test4', $with_eof);
$cached = $backend->get('test4');
$this->assert(is_object($cached), "Backend returned an object for cache id test4.");
$this->assertIdentical($with_eof, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.');
$this->assertIdentical(FALSE, $backend->get('test5'), "Backend does not contain data for cache id test5.");
$with_eof_and_semicolon = array('foo' => "\nEOF;\ndata");
$backend->set('test5', $with_eof_and_semicolon);
$cached = $backend->get('test5');
$this->assert(is_object($cached), "Backend returned an object for cache id test5.");
$this->assertIdentical($with_eof_and_semicolon, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.');
$with_variable = array('foo' => '$bar');
$backend->set('test6', $with_variable);
$cached = $backend->get('test6');
$this->assert(is_object($cached), "Backend returned an object for cache id test6.");
$this->assertIdentical($with_variable, $cached->data);
// Make sure that a cached object is not affected by changing the original.
$data = new \stdClass();
$data->value = 1;
$data->obj = new \stdClass();
$data->obj->value = 2;
$backend->set('test7', $data);
$expected_data = clone $data;
// Add a property to the original. It should not appear in the cached data.
$data->this_should_not_be_in_the_cache = TRUE;
$cached = $backend->get('test7');
$this->assert(is_object($cached), "Backend returned an object for cache id test7.");
$this->assertEqual($expected_data, $cached->data);
$this->assertFalse(isset($cached->data->this_should_not_be_in_the_cache));
// Add a property to the cache data. It should not appear when we fetch
// the data from cache again.
$cached->data->this_should_not_be_in_the_cache = TRUE;
$fresh_cached = $backend->get('test7');
$this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache));
// Check with a long key.
$cid = str_repeat('a', 300);
$backend->set($cid, 'test');
$this->assertEqual('test', $backend->get($cid)->data);
// Check that the cache key is case sensitive.
$backend->set('TEST8', 'value');
$this->assertEqual('value', $backend->get('TEST8')->data);
$this->assertFalse($backend->get('test8'));
// Calling ::set() with invalid cache tags.
try {
$backend->set('exception_test', 'value', Cache::PERMANENT, ['node' => [3, 5, 7]]);
$this->fail('::set() was called with invalid cache tags, no exception was thrown.');
}
catch (\LogicException $e) {
$this->pass('::set() was called with invalid cache tags, an exception was thrown.');
}
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::delete().
*/
public function testDelete() {
$backend = $this->getCacheBackend();
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1.");
$backend->set('test1', 7);
$this->assert(is_object($backend->get('test1')), "Backend returned an object for cache id test1.");
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2.");
$backend->set('test2', 3);
$this->assert(is_object($backend->get('test2')), "Backend returned an object for cache id %cid.");
$backend->delete('test1');
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1 after deletion.");
$this->assert(is_object($backend->get('test2')), "Backend still has an object for cache id test2.");
$backend->delete('test2');
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2 after deletion.");
$long_cid = str_repeat('a', 300);
$backend->set($long_cid, 'test');
$backend->delete($long_cid);
$this->assertIdentical(FALSE, $backend->get($long_cid), "Backend does not contain data for long cache id after deletion.");
}
/**
* Tests data type preservation.
*/
public function testValueTypeIsKept() {
$backend = $this->getCacheBackend();
$variables = array(
'test1' => 1,
'test2' => '0',
'test3' => '',
'test4' => 12.64,
'test5' => FALSE,
'test6' => array(1,2,3),
);
// Create cache entries.
foreach ($variables as $cid => $data) {
$backend->set($cid, $data);
}
// Retrieve and test cache objects.
foreach ($variables as $cid => $value) {
$object = $backend->get($cid);
$this->assert(is_object($object), sprintf("Backend returned an object for cache id %s.", $cid));
$this->assertIdentical($value, $object->data, sprintf("Data of cached id %s kept is identical in type and value", $cid));
}
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function testGetMultiple() {
$backend = $this->getCacheBackend();
// Set numerous testing keys.
$long_cid = str_repeat('a', 300);
$backend->set('test1', 1);
$backend->set('test2', 3);
$backend->set('test3', 5);
$backend->set('test4', 7);
$backend->set('test5', 11);
$backend->set('test6', 13);
$backend->set('test7', 17);
$backend->set($long_cid, 300);
// Mismatch order for harder testing.
$reference = array(
'test3',
'test7',
'test21', // Cid does not exist.
'test6',
'test19', // Cid does not exist until added before second getMultiple().
'test2',
);
$cids = $reference;
$ret = $backend->getMultiple($cids);
// Test return - ensure it contains existing cache ids.
$this->assert(isset($ret['test2']), "Existing cache id test2 is set.");
$this->assert(isset($ret['test3']), "Existing cache id test3 is set.");
$this->assert(isset($ret['test6']), "Existing cache id test6 is set.");
$this->assert(isset($ret['test7']), "Existing cache id test7 is set.");
// Test return - ensure that objects has expected properties.
$this->assertTrue($ret['test2']->valid, 'Item is marked as valid.');
$this->assertTrue($ret['test2']->created >= REQUEST_TIME && $ret['test2']->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($ret['test2']->expire, Cache::PERMANENT, 'Expire time is correct.');
// Test return - ensure it does not contain nonexistent cache ids.
$this->assertFalse(isset($ret['test19']), "Nonexistent cache id test19 is not set.");
$this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set.");
// Test values.
$this->assertIdentical($ret['test2']->data, 3, "Existing cache id test2 has the correct value.");
$this->assertIdentical($ret['test3']->data, 5, "Existing cache id test3 has the correct value.");
$this->assertIdentical($ret['test6']->data, 13, "Existing cache id test6 has the correct value.");
$this->assertIdentical($ret['test7']->data, 17, "Existing cache id test7 has the correct value.");
// Test $cids array - ensure it contains cache id's that do not exist.
$this->assert(in_array('test19', $cids), "Nonexistent cache id test19 is in cids array.");
$this->assert(in_array('test21', $cids), "Nonexistent cache id test21 is in cids array.");
// Test $cids array - ensure it does not contain cache id's that exist.
$this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array.");
$this->assertFalse(in_array('test3', $cids), "Existing cache id test3 is not in cids array.");
$this->assertFalse(in_array('test6', $cids), "Existing cache id test6 is not in cids array.");
$this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array.");
// Test a second time after deleting and setting new keys which ensures that
// if the backend uses statics it does not cause unexpected results.
$backend->delete('test3');
$backend->delete('test6');
$backend->set('test19', 57);
$cids = $reference;
$ret = $backend->getMultiple($cids);
// Test return - ensure it contains existing cache ids.
$this->assert(isset($ret['test2']), "Existing cache id test2 is set");
$this->assert(isset($ret['test7']), "Existing cache id test7 is set");
$this->assert(isset($ret['test19']), "Added cache id test19 is set");
// Test return - ensure it does not contain nonexistent cache ids.
$this->assertFalse(isset($ret['test3']), "Deleted cache id test3 is not set");
$this->assertFalse(isset($ret['test6']), "Deleted cache id test6 is not set");
$this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set");
// Test values.
$this->assertIdentical($ret['test2']->data, 3, "Existing cache id test2 has the correct value.");
$this->assertIdentical($ret['test7']->data, 17, "Existing cache id test7 has the correct value.");
$this->assertIdentical($ret['test19']->data, 57, "Added cache id test19 has the correct value.");
// Test $cids array - ensure it contains cache id's that do not exist.
$this->assert(in_array('test3', $cids), "Deleted cache id test3 is in cids array.");
$this->assert(in_array('test6', $cids), "Deleted cache id test6 is in cids array.");
$this->assert(in_array('test21', $cids), "Nonexistent cache id test21 is in cids array.");
// Test $cids array - ensure it does not contain cache id's that exist.
$this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array.");
$this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array.");
$this->assertFalse(in_array('test19', $cids), "Added cache id test19 is not in cids array.");
// Test with a long $cid and non-numeric array key.
$cids = array('key:key' => $long_cid);
$return = $backend->getMultiple($cids);
$this->assertEqual(300, $return[$long_cid]->data);
$this->assertTrue(empty($cids));
}
/**
* Tests \Drupal\Core\Cache\CacheBackendInterface::setMultiple().
*/
public function testSetMultiple() {
$backend = $this->getCacheBackend();
$future_expiration = REQUEST_TIME + 100;
// Set multiple testing keys.
$backend->set('cid_1', 'Some other value');
$items = array(
'cid_1' => array('data' => 1),
'cid_2' => array('data' => 2),
'cid_3' => array('data' => array(1, 2)),
'cid_4' => array('data' => 1, 'expire' => $future_expiration),
'cid_5' => array('data' => 1, 'tags' => array('test:a', 'test:b')),
);
$backend->setMultiple($items);
$cids = array_keys($items);
$cached = $backend->getMultiple($cids);
$this->assertEqual($cached['cid_1']->data, $items['cid_1']['data'], 'Over-written cache item set correctly.');
$this->assertTrue($cached['cid_1']->valid, 'Item is marked as valid.');
$this->assertTrue($cached['cid_1']->created >= REQUEST_TIME && $cached['cid_1']->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached['cid_1']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.');
$this->assertEqual($cached['cid_2']->data, $items['cid_2']['data'], 'New cache item set correctly.');
$this->assertEqual($cached['cid_2']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.');
$this->assertEqual($cached['cid_3']->data, $items['cid_3']['data'], 'New cache item with serialized data set correctly.');
$this->assertEqual($cached['cid_3']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.');
$this->assertEqual($cached['cid_4']->data, $items['cid_4']['data'], 'New cache item set correctly.');
$this->assertEqual($cached['cid_4']->expire, $future_expiration, 'Cache expiration has been correctly set.');
$this->assertEqual($cached['cid_5']->data, $items['cid_5']['data'], 'New cache item set correctly.');
// Calling ::setMultiple() with invalid cache tags.
try {
$items = [
'exception_test_1' => array('data' => 1, 'tags' => []),
'exception_test_2' => array('data' => 2, 'tags' => ['valid']),
'exception_test_3' => array('data' => 3, 'tags' => ['node' => [3, 5, 7]]),
];
$backend->setMultiple($items);
$this->fail('::setMultiple() was called with invalid cache tags, no exception was thrown.');
}
catch (\LogicException $e) {
$this->pass('::setMultiple() was called with invalid cache tags, an exception was thrown.');
}
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::delete() and
* Drupal\Core\Cache\CacheBackendInterface::deleteMultiple().
*/
public function testDeleteMultiple() {
$backend = $this->getCacheBackend();
// Set numerous testing keys.
$backend->set('test1', 1);
$backend->set('test2', 3);
$backend->set('test3', 5);
$backend->set('test4', 7);
$backend->set('test5', 11);
$backend->set('test6', 13);
$backend->set('test7', 17);
$backend->delete('test1');
$backend->delete('test23'); // Nonexistent key should not cause an error.
$backend->deleteMultiple(array(
'test3',
'test5',
'test7',
'test19', // Nonexistent key should not cause an error.
'test21', // Nonexistent key should not cause an error.
));
// Test if expected keys have been deleted.
$this->assertIdentical(FALSE, $backend->get('test1'), "Cache id test1 deleted.");
$this->assertIdentical(FALSE, $backend->get('test3'), "Cache id test3 deleted.");
$this->assertIdentical(FALSE, $backend->get('test5'), "Cache id test5 deleted.");
$this->assertIdentical(FALSE, $backend->get('test7'), "Cache id test7 deleted.");
// Test if expected keys exist.
$this->assertNotIdentical(FALSE, $backend->get('test2'), "Cache id test2 exists.");
$this->assertNotIdentical(FALSE, $backend->get('test4'), "Cache id test4 exists.");
$this->assertNotIdentical(FALSE, $backend->get('test6'), "Cache id test6 exists.");
// Test if that expected keys do not exist.
$this->assertIdentical(FALSE, $backend->get('test19'), "Cache id test19 does not exist.");
$this->assertIdentical(FALSE, $backend->get('test21'), "Cache id test21 does not exist.");
// Calling deleteMultiple() with an empty array should not cause an error.
$this->assertFalse($backend->deleteMultiple(array()));
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function testDeleteAll() {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->deleteAll();
$this->assertFalse($backend_a->get('test1'), 'First key has been deleted.');
$this->assertFalse($backend_a->get('test2'), 'Second key has been deleted.');
$this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.');
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::invalidate() and
* Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple().
*/
function testInvalidate() {
$backend = $this->getCacheBackend();
$backend->set('test1', 1);
$backend->set('test2', 2);
$backend->set('test3', 2);
$backend->set('test4', 2);
$reference = array('test1', 'test2', 'test3', 'test4');
$cids = $reference;
$ret = $backend->getMultiple($cids);
$this->assertEqual(count($ret), 4, 'Four items returned.');
$backend->invalidate('test1');
$backend->invalidateMultiple(array('test2', 'test3'));
$cids = $reference;
$ret = $backend->getMultiple($cids);
$this->assertEqual(count($ret), 1, 'Only one item element returned.');
$cids = $reference;
$ret = $backend->getMultiple($cids, TRUE);
$this->assertEqual(count($ret), 4, 'Four items returned.');
// Calling invalidateMultiple() with an empty array should not cause an
// error.
$this->assertFalse($backend->invalidateMultiple(array()));
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
function testInvalidateTags() {
$backend = $this->getCacheBackend();
// Create two cache entries with the same tag and tag value.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:2'));
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:2'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Two cache items were created.');
// Invalidate test_tag of value 1. This should invalidate both entries.
Cache::invalidateTags(array('test_tag:2'));
$this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two cache items invalidated after invalidating a cache tag.');
$this->assertTrue($backend->get('test_cid_invalidate1', TRUE) && $backend->get('test_cid_invalidate2', TRUE), 'Cache items not deleted after invalidating a cache tag.');
// Create two cache entries with the same tag and an array tag value.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:1'));
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:1'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Two cache items were created.');
// Invalidate test_tag of value 1. This should invalidate both entries.
Cache::invalidateTags(array('test_tag:1'));
$this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two caches removed after invalidating a cache tag.');
$this->assertTrue($backend->get('test_cid_invalidate1', TRUE) && $backend->get('test_cid_invalidate2', TRUE), 'Cache items not deleted after invalidating a cache tag.');
// Create three cache entries with a mix of tags and tag values.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:1'));
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:2'));
$backend->set('test_cid_invalidate3', $this->defaultValue, Cache::PERMANENT, array('test_tag_foo:3'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2') && $backend->get('test_cid_invalidate3'), 'Three cached items were created.');
Cache::invalidateTags(array('test_tag_foo:3'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Cache items not matching the tag were not invalidated.');
$this->assertFalse($backend->get('test_cid_invalidated3'), 'Cached item matching the tag was removed.');
// Create cache entry in multiple bins. Two cache entries
// (test_cid_invalidate1 and test_cid_invalidate2) still exist from previous
// tests.
$tags = array('test_tag:1', 'test_tag:2', 'test_tag:3');
$bins = array('path', 'bootstrap', 'page');
foreach ($bins as $bin) {
$this->getCacheBackend($bin)->set('test', $this->defaultValue, Cache::PERMANENT, $tags);
$this->assertTrue($this->getCacheBackend($bin)->get('test'), 'Cache item was set in bin.');
}
Cache::invalidateTags(array('test_tag:2'));
// Test that the cache entry has been invalidated in multiple bins.
foreach ($bins as $bin) {
$this->assertFalse($this->getCacheBackend($bin)->get('test'), 'Tag invalidation affected item in bin.');
}
// Test that the cache entry with a matching tag has been invalidated.
$this->assertFalse($this->getCacheBackend($bin)->get('test_cid_invalidate2'), 'Cache items matching tag were invalidated.');
// Test that the cache entry with without a matching tag still exists.
$this->assertTrue($this->getCacheBackend($bin)->get('test_cid_invalidate1'), 'Cache items not matching tag were not invalidated.');
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function testInvalidateAll() {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->invalidateAll();
$this->assertFalse($backend_a->get('test1'), 'First key has been invalidated.');
$this->assertFalse($backend_a->get('test2'), 'Second key has been invalidated.');
$this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.');
$this->assertTrue($backend_a->get('test1', TRUE), 'First key has not been deleted.');
$this->assertTrue($backend_a->get('test2', TRUE), 'Second key has not been deleted.');
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::removeBin().
*/
public function testRemoveBin() {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->removeBin();
$this->assertFalse($backend_a->get('test1'), 'First key has been deleted.');
$this->assertFalse($backend_a->get('test2', TRUE), 'Second key has been deleted.');
$this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.');
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\MemoryBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\MemoryBackend;
/**
* Unit test of the memory cache backend using the generic cache unit test base.
*
* @group Cache
*/
class MemoryBackendUnitTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of MemoryBackend.
*
* @return
* A new MemoryBackend object.
*/
protected function createCacheBackend($bin) {
$backend = new MemoryBackend($bin);
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\PageCacheTagsTestBase.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Utility\SafeMarkup;
/**
* Provides helper methods for page cache tags tests.
*/
abstract class PageCacheTagsTestBase extends WebTestBase {
/**
* {@inheritdoc}
*
* Always enable header dumping in page cache tags tests, this aids debugging.
*/
protected $dumpHeaders = TRUE;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Enable page caching.
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 3600);
$config->save();
}
/**
* Verify that when loading a given page, it's a page cache hit or miss.
*
* @param \Drupal\Core\Url $url
* The page for this URL will be loaded.
* @param string $hit_or_miss
* 'HIT' if a page cache hit is expected, 'MISS' otherwise.
*
* @param array|FALSE $tags
* When expecting a page cache hit, you may optionally specify an array of
* expected cache tags. While FALSE, the cache tags will not be verified.
*/
protected function verifyPageCache(Url $url, $hit_or_miss, $tags = FALSE) {
$this->drupalGet($url);
$message = SafeMarkup::format('Page cache @hit_or_miss for %path.', array('@hit_or_miss' => $hit_or_miss, '%path' => $url->toString()));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), $hit_or_miss, $message);
if ($hit_or_miss === 'HIT' && is_array($tags)) {
$absolute_url = $url->setAbsolute()->toString();
$cid_parts = array($absolute_url, 'html');
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('render')->get($cid);
sort($cache_entry->tags);
$tags = array_unique($tags);
sort($tags);
$this->assertIdentical($cache_entry->tags, $tags);
}
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\PhpBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\PhpBackend;
/**
* Unit test of the PHP cache backend using the generic cache unit test base.
*
* @group Cache
*/
class PhpBackendUnitTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of MemoryBackend.
*
* @return
* A new MemoryBackend object.
*/
protected function createCacheBackend($bin) {
$backend = new PhpBackend($bin, \Drupal::service('cache_tags.invalidator.checksum'));
return $backend;
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\AddFeedTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* Make sure that attaching feeds works correctly with various constructs.
*
* @group Common
*/
class AddFeedTest extends WebTestBase {
/**
* Tests attaching feeds with paths, URLs, and titles.
*/
function testBasicFeedAddNoTitle() {
$path = $this->randomMachineName(12);
$external_url = 'http://' . $this->randomMachineName(12) . '/' . $this->randomMachineName(12);
$fully_qualified_local_url = Url::fromUri('base:' . $this->randomMachineName(12), array('absolute' => TRUE))->toString();
$path_for_title = $this->randomMachineName(12);
$external_for_title = 'http://' . $this->randomMachineName(12) . '/' . $this->randomMachineName(12);
$fully_qualified_for_title = Url::fromUri('base:' . $this->randomMachineName(12), array('absolute' => TRUE))->toString();
$urls = array(
'path without title' => array(
'url' => Url::fromUri('base:' . $path, array('absolute' => TRUE))->toString(),
'title' => '',
),
'external URL without title' => array(
'url' => $external_url,
'title' => '',
),
'local URL without title' => array(
'url' => $fully_qualified_local_url,
'title' => '',
),
'path with title' => array(
'url' => Url::fromUri('base:' . $path_for_title, array('absolute' => TRUE))->toString(),
'title' => $this->randomMachineName(12),
),
'external URL with title' => array(
'url' => $external_for_title,
'title' => $this->randomMachineName(12),
),
'local URL with title' => array(
'url' => $fully_qualified_for_title,
'title' => $this->randomMachineName(12),
),
);
$build = [];
foreach ($urls as $feed_info) {
$build['#attached']['feed'][] = [$feed_info['url'], $feed_info['title']];
}
drupal_process_attached($build);
$this->setRawContent(drupal_get_html_head());
foreach ($urls as $description => $feed_info) {
$this->assertPattern($this->urlToRSSLinkPattern($feed_info['url'], $feed_info['title']), format_string('Found correct feed header for %description', array('%description' => $description)));
}
}
/**
* Creates a pattern representing the RSS feed in the page.
*/
function urlToRSSLinkPattern($url, $title = '') {
// Escape any regular expression characters in the URL ('?' is the worst).
$url = preg_replace('/([+?.*])/', '[$0]', $url);
$generated_pattern = '%<link +href="' . $url . '" +rel="alternate" +title="' . $title . '" +type="application/rss.xml" */>%';
return $generated_pattern;
}
/**
* Checks that special characters are correctly escaped.
*
* @see https://www.drupal.org/node/1211668
*/
function testFeedIconEscaping() {
$variables = array(
'#theme' => 'feed_icon',
'#url' => 'node',
'#title' => '<>&"\'',
);
$text = \Drupal::service('renderer')->renderRoot($variables);
preg_match('/title="(.*?)"/', $text, $matches);
$this->assertEqual($matches[1], 'Subscribe to &amp;&quot;&#039;', 'feed_icon template escapes reserved HTML characters.');
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\AlterTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\simpletest\WebTestBase;
/**
* Tests alteration of arguments passed to \Drupal::moduleHandler->alter().
*
* @group Common
*/
class AlterTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('block', 'common_test');
/**
* Tests if the theme has been altered.
*/
function testDrupalAlter() {
// This test depends on Bartik, so make sure that it is always the current
// active theme.
\Drupal::service('theme_handler')->install(array('bartik'));
\Drupal::theme()->setActiveTheme(\Drupal::service('theme.initialization')->initTheme('bartik'));
$array = array('foo' => 'bar');
$entity = new \stdClass();
$entity->foo = 'bar';
// Verify alteration of a single argument.
$array_copy = $array;
$array_expected = array('foo' => 'Drupal theme');
\Drupal::moduleHandler()->alter('drupal_alter', $array_copy);
\Drupal::theme()->alter('drupal_alter', $array_copy);
$this->assertEqual($array_copy, $array_expected, 'Single array was altered.');
$entity_copy = clone $entity;
$entity_expected = clone $entity;
$entity_expected->foo = 'Drupal theme';
\Drupal::moduleHandler()->alter('drupal_alter', $entity_copy);
\Drupal::theme()->alter('drupal_alter', $entity_copy);
$this->assertEqual($entity_copy, $entity_expected, 'Single object was altered.');
// Verify alteration of multiple arguments.
$array_copy = $array;
$array_expected = array('foo' => 'Drupal theme');
$entity_copy = clone $entity;
$entity_expected = clone $entity;
$entity_expected->foo = 'Drupal theme';
$array2_copy = $array;
$array2_expected = array('foo' => 'Drupal theme');
\Drupal::moduleHandler()->alter('drupal_alter', $array_copy, $entity_copy, $array2_copy);
\Drupal::theme()->alter('drupal_alter', $array_copy, $entity_copy, $array2_copy);
$this->assertEqual($array_copy, $array_expected, 'First argument to \Drupal::moduleHandler->alter() was altered.');
$this->assertEqual($entity_copy, $entity_expected, 'Second argument to \Drupal::moduleHandler->alter() was altered.');
$this->assertEqual($array2_copy, $array2_expected, 'Third argument to \Drupal::moduleHandler->alter() was altered.');
// Verify alteration order when passing an array of types to \Drupal::moduleHandler->alter().
// common_test_module_implements_alter() places 'block' implementation after
// other modules.
$array_copy = $array;
$array_expected = array('foo' => 'Drupal block theme');
\Drupal::moduleHandler()->alter(array('drupal_alter', 'drupal_alter_foo'), $array_copy);
\Drupal::theme()->alter(array('drupal_alter', 'drupal_alter_foo'), $array_copy);
$this->assertEqual($array_copy, $array_expected, 'hook_TYPE_alter() implementations ran in correct order.');
}
}

View file

@ -0,0 +1,485 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\AttachedAssetsTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\simpletest\KernelTestBase;
/**
* Tests #attached assets: attached asset libraries and JavaScript settings.
*
* i.e. tests:
*
* @code
* $build['#attached']['library'] =
* $build['#attached']['drupalSettings'] =
* @endcode
*
* @group Common
* @group Asset
*/
class AttachedAssetsTest extends KernelTestBase {
/**
* The asset resolver service.
*
* @var \Drupal\Core\Asset\AssetResolverInterface
*/
protected $assetResolver;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* {@inheritdoc}
*/
public static $modules = array('language', 'simpletest', 'common_test', 'system');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', array('router'));
$this->container->get('router.builder')->rebuild();
$this->assetResolver = $this->container->get('asset.resolver');
$this->renderer = $this->container->get('renderer');
}
/**
* Tests that default CSS and JavaScript is empty.
*/
function testDefault() {
$assets = new AttachedAssets();
$this->assertEqual(array(), $this->assetResolver->getCssAssets($assets, FALSE), 'Default CSS is empty.');
list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, FALSE);
$this->assertEqual(array(), $js_assets_header, 'Default header JavaScript is empty.');
$this->assertEqual(array(), $js_assets_footer, 'Default footer JavaScript is empty.');
}
/**
* Tests non-existing libraries.
*/
function testLibraryUnknown() {
$build['#attached']['library'][] = 'core/unknown';
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertIdentical([], $this->assetResolver->getJsAssets($assets, FALSE)[0], 'Unknown library was not added to the page.');
}
/**
* Tests adding a CSS and a JavaScript file.
*/
function testAddFiles() {
$build['#attached']['library'][] = 'common_test/files';
$assets = AttachedAssets::createFromRenderArray($build);
$css = $this->assetResolver->getCssAssets($assets, FALSE);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/bar.css', $css), 'CSS files are correctly added.');
$this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/foo.js', $js), 'JavaScript files are correctly added.');
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_css = $this->renderer->renderPlain($css_render_array);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
$this->assertNotIdentical(strpos($rendered_css, '<link rel="stylesheet" href="' . file_create_url('core/modules/system/tests/modules/common_test/bar.css') . '?' . $query_string . '" media="all" />'), FALSE, 'Rendering an external CSS file.');
$this->assertNotIdentical(strpos($rendered_js, '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/foo.js') . '?' . $query_string . '"></script>'), FALSE, 'Rendering an external JavaScript file.');
}
/**
* Tests adding JavaScript settings.
*/
function testAddJsSettings() {
// Add a file in order to test default settings.
$build['#attached']['library'][] = 'core/drupalSettings';
$assets = AttachedAssets::createFromRenderArray($build);
$javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$this->assertTrue(array_key_exists('currentPath', $javascript['drupalSettings']['data']['path']), 'The current path JavaScript setting is set correctly.');
$assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]);
$javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$this->assertEqual(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.');
$this->assertEqual('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.');
}
/**
* Tests adding external CSS and JavaScript files.
*/
function testAddExternalFiles() {
$build['#attached']['library'][] = 'common_test/external';
$assets = AttachedAssets::createFromRenderArray($build);
$css = $this->assetResolver->getCssAssets($assets, FALSE);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$this->assertTrue(array_key_exists('http://example.com/stylesheet.css', $css), 'External CSS files are correctly added.');
$this->assertTrue(array_key_exists('http://example.com/script.js', $js), 'External JavaScript files are correctly added.');
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_css = $this->renderer->renderPlain($css_render_array);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$this->assertNotIdentical(strpos($rendered_css, '<link rel="stylesheet" href="http://example.com/stylesheet.css" media="all" />'), FALSE, 'Rendering an external CSS file.');
$this->assertNotIdentical(strpos($rendered_js, '<script src="http://example.com/script.js"></script>'), FALSE, 'Rendering an external JavaScript file.');
}
/**
* Tests adding JavaScript files with additional attributes.
*/
function testAttributes() {
$build['#attached']['library'][] = 'common_test/js-attributes';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
$expected_2 = '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
$this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.');
$this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.');
}
/**
* Tests that attributes are maintained when JS aggregation is enabled.
*/
function testAggregatedAttributes() {
$build['#attached']['library'][] = 'common_test/js-attributes';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, TRUE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
$expected_2 = '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/deferred-internal.js') . '?v=1" defer bar="foo"></script>';
$this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.');
$this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.');
}
/**
* Integration test for CSS/JS aggregation.
*/
function testAggregation() {
$build['#attached']['library'][] = 'core/drupal.timezone';
$build['#attached']['library'][] = 'core/drupal.vertical-tabs';
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertEqual(1, count($this->assetResolver->getCssAssets($assets, TRUE)), 'There is a sole aggregated CSS asset.');
list($header_js, $footer_js) = $this->assetResolver->getJsAssets($assets, TRUE);
$this->assertEqual([], \Drupal::service('asset.js.collection_renderer')->render($header_js), 'There are 0 JavaScript assets in the header.');
$rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js);
$this->assertTrue(
count($rendered_footer_js) == 2
&& substr($rendered_footer_js[0]['#value'], 0, 20) === 'var drupalSettings ='
&& substr($rendered_footer_js[1]['#attributes']['src'], 0, 7) === 'http://',
'There are 2 JavaScript assets in the footer: one with drupalSettings, one with the sole aggregated JavaScript asset.'
);
}
/**
* Tests JavaScript settings.
*/
function testSettings() {
$build = array();
$build['#attached']['library'][] = 'core/drupalSettings';
// Nonsensical value to verify if it's possible to override path settings.
$build['#attached']['drupalSettings']['path']['pathPrefix'] = 'yarhar';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
// Parse the generated drupalSettings <script> back to a PHP representation.
$startToken = 'drupalSettings = ';
$endToken = '}';
$start = strpos($rendered_js, $startToken) + strlen($startToken);
$end = strrpos($rendered_js, $endToken);
$json = Unicode::substr($rendered_js, $start, $end - $start + 1);
$parsed_settings = Json::decode($json);
// Test whether the settings for core/drupalSettings are available.
$this->assertTrue(isset($parsed_settings['path']['baseUrl']), 'drupalSettings.path.baseUrl is present.');
$this->assertTrue(isset($parsed_settings['path']['scriptPath']), 'drupalSettings.path.scriptPath is present.');
$this->assertIdentical($parsed_settings['path']['pathPrefix'], 'yarhar', 'drupalSettings.path.pathPrefix is present and has the correct (overridden) value.');
$this->assertIdentical($parsed_settings['path']['currentPath'], '', 'drupalSettings.path.currentPath is present and has the correct value.');
$this->assertIdentical($parsed_settings['path']['currentPathIsAdmin'], FALSE, 'drupalSettings.path.currentPathIsAdmin is present and has the correct value.');
$this->assertIdentical($parsed_settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is present and has the correct value.');
$this->assertIdentical($parsed_settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is present and has the correct value.');
// Tests whether altering JavaScript settings via hook_js_settings_alter()
// is working as expected.
// @see common_test_js_settings_alter()
$this->assertIdentical($parsed_settings['pluralDelimiter'], '☃');
$this->assertIdentical($parsed_settings['foo'], 'bar');
}
/**
* Tests JS assets depending on the 'core/<head>' virtual library.
*/
function testHeaderHTML() {
$build['#attached']['library'][] = 'common_test/js-header';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[0];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
$this->assertNotIdentical(strpos($rendered_js, '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/header.js') . '?' . $query_string . '"></script>'), FALSE, 'The JS asset in common_test/js-header appears in the header.');
$this->assertNotIdentical(strpos($rendered_js, '<script src="' . file_create_url('core/misc/drupal.js')), FALSE, 'The JS asset of the direct dependency (core/drupal) of common_test/js-header appears in the header.');
$this->assertNotIdentical(strpos($rendered_js, '<script src="' . file_create_url('core/assets/vendor/domready/ready.min.js')), FALSE, 'The JS asset of the indirect dependency (core/domready) of common_test/js-header appears in the header.');
}
/**
* Tests that for assets with cache = FALSE, Drupal sets preprocess = FALSE.
*/
function testNoCache() {
$build['#attached']['library'][] = 'common_test/no-cache';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$this->assertFalse($js['core/modules/system/tests/modules/common_test/nocache.js']['preprocess'], 'Setting cache to FALSE sets preprocess to FALSE when adding JavaScript.');
}
/**
* Tests adding JavaScript within conditional comments.
*
* @see \Drupal\Core\Render\Element\HtmlTag::preRenderConditionalComments()
*/
function testBrowserConditionalComments() {
$default_query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
$build['#attached']['library'][] = 'common_test/browsers';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$expected_1 = "<!--[if lte IE 8]>\n" . '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/old-ie.js') . '?' . $default_query_string . '"></script>' . "\n<![endif]-->";
$expected_2 = "<!--[if !IE]><!-->\n" . '<script src="' . file_create_url('core/modules/system/tests/modules/common_test/no-ie.js') . '?' . $default_query_string . '"></script>' . "\n<!--<![endif]-->";
$this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered JavaScript within downlevel-hidden conditional comments.');
$this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered JavaScript within downlevel-revealed conditional comments.');
}
/**
* Tests JavaScript versioning.
*/
function testVersionQueryString() {
$build['#attached']['library'][] = 'core/backbone';
$build['#attached']['library'][] = 'core/domready';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$this->assertTrue(strpos($rendered_js, 'core/assets/vendor/backbone/backbone-min.js?v=1.1.2') > 0 && strpos($rendered_js, 'core/assets/vendor/domready/ready.min.js?v=1.0.8') > 0 , 'JavaScript version identifiers correctly appended to URLs');
}
/**
* Tests JavaScript and CSS asset ordering.
*/
function testRenderOrder() {
$build['#attached']['library'][] = 'common_test/order';
$assets = AttachedAssets::createFromRenderArray($build);
// Construct the expected result from the regex.
$expected_order_js = [
"-8_1",
"-8_2",
"-8_3",
"-8_4",
"-5_1", // The external script.
"-3_1",
"-3_2",
"0_1",
"0_2",
"0_3",
];
// Retrieve the rendered JavaScript and test against the regex.
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$matches = array();
if (preg_match_all('/weight_([-0-9]+_[0-9]+)/', $rendered_js, $matches)) {
$result = $matches[1];
}
else {
$result = array();
}
$this->assertIdentical($result, $expected_order_js, 'JavaScript is added in the expected weight order.');
// Construct the expected result from the regex.
$expected_order_css = [
// Base.
'base_weight_-101_1',
'base_weight_-8_1',
'layout_weight_-101_1',
'base_weight_0_1',
'base_weight_0_2',
// Layout.
'layout_weight_-8_1',
'component_weight_-101_1',
'layout_weight_0_1',
'layout_weight_0_2',
// Component.
'component_weight_-8_1',
'state_weight_-101_1',
'component_weight_0_1',
'component_weight_0_2',
// State.
'state_weight_-8_1',
'theme_weight_-101_1',
'state_weight_0_1',
'state_weight_0_2',
// Theme.
'theme_weight_-8_1',
'theme_weight_0_1',
'theme_weight_0_2',
];
// Retrieve the rendered CSS and test against the regex.
$css = $this->assetResolver->getCssAssets($assets, FALSE);
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$rendered_css = $this->renderer->renderPlain($css_render_array);
$matches = array();
if (preg_match_all('/([a-z]+)_weight_([-0-9]+_[0-9]+)/', $rendered_css, $matches)) {
$result = $matches[0];
}
else {
$result = array();
}
$this->assertIdentical($result, $expected_order_css, 'CSS is added in the expected weight order.');
}
/**
* Tests rendering the JavaScript with a file's weight above jQuery's.
*/
function testRenderDifferentWeight() {
// If a library contains assets A and B, and A is listed first, then B can
// still make itself appear first by defining a lower weight.
$build['#attached']['library'][] = 'core/jquery';
$build['#attached']['library'][] = 'common_test/weight';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$this->assertTrue(strpos($rendered_js, 'lighter.css') < strpos($rendered_js, 'first.js'), 'Lighter CSS assets are rendered first.');
$this->assertTrue(strpos($rendered_js, 'lighter.js') < strpos($rendered_js, 'first.js'), 'Lighter JavaScript assets are rendered first.');
$this->assertTrue(strpos($rendered_js, 'before-jquery.js') < strpos($rendered_js, 'core/assets/vendor/jquery/jquery.min.js'), 'Rendering a JavaScript file above jQuery.');
}
/**
* Tests altering a JavaScript's weight via hook_js_alter().
*
* @see simpletest_js_alter()
*/
function testAlter() {
// Add both tableselect.js and simpletest.js.
$build['#attached']['library'][] = 'core/drupal.tableselect';
$build['#attached']['library'][] = 'simpletest/drupal.simpletest';
$assets = AttachedAssets::createFromRenderArray($build);
// Render the JavaScript, testing if simpletest.js was altered to be before
// tableselect.js. See simpletest_js_alter() to see where this alteration
// takes place.
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$this->assertTrue(strpos($rendered_js, 'simpletest.js') < strpos($rendered_js, 'core/misc/tableselect.js'), 'Altering JavaScript weight through the alter hook.');
}
/**
* Adds a JavaScript library to the page and alters it.
*
* @see common_test_library_info_alter()
*/
function testLibraryAlter() {
// Verify that common_test altered the title of Farbtastic.
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
$library = $library_discovery->getLibraryByName('core', 'jquery.farbtastic');
$this->assertEqual($library['version'], '0.0', 'Registered libraries were altered.');
// common_test_library_info_alter() also added a dependency on jQuery Form.
$build['#attached']['library'][] = 'core/jquery.farbtastic';
$assets = AttachedAssets::createFromRenderArray($build);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$this->assertTrue(strpos($rendered_js, 'core/assets/vendor/jquery-form/jquery.form.min.js'), 'Altered library dependencies are added to the page.');
}
/**
* Dynamically defines an asset library and alters it.
*/
function testDynamicLibrary() {
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
// Retrieve a dynamic library definition.
// @see common_test_library_info_build()
\Drupal::state()->set('common_test.library_info_build_test', TRUE);
$library_discovery->clearCachedDefinitions();
$dynamic_library = $library_discovery->getLibraryByName('common_test', 'dynamic_library');
$this->assertTrue(is_array($dynamic_library));
if ($this->assertTrue(isset($dynamic_library['version']))) {
$this->assertIdentical('1.0', $dynamic_library['version']);
}
// Make sure the dynamic library definition could be altered.
// @see common_test_library_info_alter()
if ($this->assertTrue(isset($dynamic_library['dependencies']))) {
$this->assertIdentical(['core/jquery'], $dynamic_library['dependencies']);
}
}
/**
* Tests that multiple modules can implement libraries with the same name.
*
* @see common_test.library.yml
*/
function testLibraryNameConflicts() {
/** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
$library_discovery = \Drupal::service('library.discovery');
$farbtastic = $library_discovery->getLibraryByName('common_test', 'jquery.farbtastic');
$this->assertEqual($farbtastic['version'], '0.1', 'Alternative libraries can be added to the page.');
}
/**
* Tests JavaScript files that have querystrings attached get added right.
*/
function testAddJsFileWithQueryString() {
$build['#attached']['library'][] = 'common_test/querystring';
$assets = AttachedAssets::createFromRenderArray($build);
$css = $this->assetResolver->getCssAssets($assets, FALSE);
$js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
$this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css), 'CSS file with query string is correctly added.');
$this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js), 'JavaScript file with query string is correctly added.');
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$rendered_css = $this->renderer->renderPlain($css_render_array);
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
$this->assertNotIdentical(strpos($rendered_css, '<link rel="stylesheet" href="' . str_replace('&', '&amp;', file_create_url('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '" media="all" />'), FALSE, 'CSS file with query string gets version query string correctly appended..');
$this->assertNotIdentical(strpos($rendered_js, '<script src="' . str_replace('&', '&amp;', file_create_url('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '"></script>'), FALSE, 'JavaScript file with query string gets version query string correctly appended.');
}
}

View file

@ -0,0 +1,121 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\FormatDateTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Tests the format_date() function.
*
* @group Common
*/
class FormatDateTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language');
/**
* Arbitrary langcode for a custom language.
*/
const LANGCODE = 'xx';
protected function setUp() {
parent::setUp('language');
$this->config('system.date')
->set('timezone.user.configurable', 1)
->save();
$formats = $this->container->get('entity.manager')
->getStorage('date_format')
->loadMultiple(array('long', 'medium', 'short'));
$formats['long']->setPattern('l, j. F Y - G:i')->save();
$formats['medium']->setPattern('j. F Y - G:i')->save();
$formats['short']->setPattern('Y M j - g:ia')->save();
$this->refreshVariables();
$this->settingsSet('locale_custom_strings_' . self::LANGCODE, array(
'' => array('Sunday' => 'domingo'),
'Long month name' => array('March' => 'marzo'),
));
ConfigurableLanguage::createFromLangcode(static::LANGCODE)->save();
$this->resetAll();
}
/**
* Tests admin-defined formats in format_date().
*/
function testAdminDefinedFormatDate() {
// Create and log in an admin user.
$this->drupalLogin($this->drupalCreateUser(array('administer site configuration')));
// Add new date format.
$edit = array(
'id' => 'example_style',
'label' => 'Example Style',
'date_format_pattern' => 'j M y',
);
$this->drupalPostForm('admin/config/regional/date-time/formats/add', $edit, t('Add format'));
// Add a second date format with a different case than the first.
$edit = array(
'id' => 'example_style_uppercase',
'label' => 'Example Style Uppercase',
'date_format_pattern' => 'j M Y',
);
$this->drupalPostForm('admin/config/regional/date-time/formats/add', $edit, t('Add format'));
$this->assertText(t('Custom date format added.'));
$timestamp = strtotime('2007-03-10T00:00:00+00:00');
$this->assertIdentical(format_date($timestamp, 'example_style', '', 'America/Los_Angeles'), '9 Mar 07');
$this->assertIdentical(format_date($timestamp, 'example_style_uppercase', '', 'America/Los_Angeles'), '9 Mar 2007');
$this->assertIdentical(format_date($timestamp, 'undefined_style'), format_date($timestamp, 'fallback'), 'Test format_date() defaulting to `fallback` when $type not found.');
}
/**
* Tests the format_date() function.
*/
function testFormatDate() {
$timestamp = strtotime('2007-03-26T00:00:00+00:00');
$this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', 'Test all parameters.');
$this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'domingo, 25-Mar-07 17:00:00 PDT', 'Test translated format.');
$this->assertIdentical(format_date($timestamp, 'custom', '\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), 'l, 25-Mar-07 17:00:00 PDT', 'Test an escaped format string.');
$this->assertIdentical(format_date($timestamp, 'custom', '\\\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), '\\domingo, 25-Mar-07 17:00:00 PDT', 'Test format containing backslash character.');
$this->assertIdentical(format_date($timestamp, 'custom', '\\\\\\l, d-M-y H:i:s T', 'America/Los_Angeles', self::LANGCODE), '\\l, 25-Mar-07 17:00:00 PDT', 'Test format containing backslash followed by escaped format string.');
$this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London', 'en'), 'Monday, 26-Mar-07 01:00:00 BST', 'Test a different time zone.');
// Change the default language and timezone.
$this->config('system.site')->set('default_langcode', static::LANGCODE)->save();
date_default_timezone_set('America/Los_Angeles');
// Reset the language manager so new negotiations attempts will fall back on
// on the new language.
$this->container->get('language_manager')->reset();
$this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'America/Los_Angeles', 'en'), 'Sunday, 25-Mar-07 17:00:00 PDT', 'Test a different language.');
$this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T', 'Europe/London'), 'Monday, 26-Mar-07 01:00:00 BST', 'Test a different time zone.');
$this->assertIdentical(format_date($timestamp, 'custom', 'l, d-M-y H:i:s T'), 'domingo, 25-Mar-07 17:00:00 PDT', 'Test custom date format.');
$this->assertIdentical(format_date($timestamp, 'long'), 'domingo, 25. marzo 2007 - 17:00', 'Test long date format.');
$this->assertIdentical(format_date($timestamp, 'medium'), '25. marzo 2007 - 17:00', 'Test medium date format.');
$this->assertIdentical(format_date($timestamp, 'short'), '2007 Mar 25 - 5:00pm', 'Test short date format.');
$this->assertIdentical(format_date($timestamp), '25. marzo 2007 - 17:00', 'Test default date format.');
// Test HTML time element formats.
$this->assertIdentical(format_date($timestamp, 'html_datetime'), '2007-03-25T17:00:00-0700', 'Test html_datetime date format.');
$this->assertIdentical(format_date($timestamp, 'html_date'), '2007-03-25', 'Test html_date date format.');
$this->assertIdentical(format_date($timestamp, 'html_time'), '17:00:00', 'Test html_time date format.');
$this->assertIdentical(format_date($timestamp, 'html_yearless_date'), '03-25', 'Test html_yearless_date date format.');
$this->assertIdentical(format_date($timestamp, 'html_week'), '2007-W12', 'Test html_week date format.');
$this->assertIdentical(format_date($timestamp, 'html_month'), '2007-03', 'Test html_month date format.');
$this->assertIdentical(format_date($timestamp, 'html_year'), '2007', 'Test html_year date format.');
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\NoJavaScriptAnonymousTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\simpletest\WebTestBase;
/**
* Tests that anonymous users are not served any JavaScript in the Standard
* installation profile.
*
* @group Common
*/
class NoJavaScriptAnonymousTest extends WebTestBase {
protected $profile = 'standard';
protected function setUp() {
parent::setUp();
// Grant the anonymous user the permission to look at user profiles.
user_role_grant_permissions('anonymous', array('access user profiles'));
}
/**
* Tests that anonymous users are not served any JavaScript.
*/
public function testNoJavaScript() {
// Create a node that is listed on the frontpage.
$this->drupalCreateNode(array(
'promote' => NODE_PROMOTED,
));
$user = $this->drupalCreateUser();
// Test frontpage.
$this->drupalGet('');
$this->assertNoJavaScriptExceptHtml5Shiv();
// Test node page.
$this->drupalGet('node/1');
$this->assertNoJavaScriptExceptHtml5Shiv();
// Test user profile page.
$this->drupalGet('user/' . $user->id());
$this->assertNoJavaScriptExceptHtml5Shiv();
}
/**
* Passes if no JavaScript is found on the page except the HTML5 shiv.
*
* The HTML5 shiv is necessary for e.g. the <article> tag which Drupal 8 uses
* to work in older browsers like Internet Explorer 8.
*/
protected function assertNoJavaScriptExceptHtml5Shiv() {
// Ensure drupalSettings is not set.
$this->assertNoRaw('var drupalSettings = {', 'drupalSettings is not set.');
// Ensure the HTML5 shiv exists.
$this->assertRaw('html5shiv/html5shiv.min.js', 'HTML5 shiv JavaScript exists.');
// Ensure no other JavaScript file exists on the page, while ignoring the
// HTML5 shiv.
$this->assertNoPattern('/(?<!html5shiv\.min)\.js/', "No other JavaScript exists.");
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\PageRenderTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Core\Render\MainContent\HtmlRenderer;
use Drupal\simpletest\KernelTestBase;
/**
* Test page rendering hooks.
*
* @group system
*/
class PageRenderTest extends KernelTestBase {
/**
* Tests hook_page_attachments() exceptions.
*/
function testHookPageAttachmentsExceptions() {
$this->enableModules(['common_test', 'system']);
$this->installSchema('system', 'router');
\Drupal::service('router.builder')->rebuild();
$this->assertPageRenderHookExceptions('common_test', 'hook_page_attachments');
}
/**
* Tests hook_page_attachments_alter() exceptions.
*/
function testHookPageAlter() {
$this->enableModules(['common_test', 'system']);
$this->installSchema('system', 'router');
\Drupal::service('router.builder')->rebuild();
$this->assertPageRenderHookExceptions('common_test', 'hook_page_attachments_alter');
}
/**
* Asserts whether expected exceptions are thrown for invalid hook implementations.
*
* @param string $module
* The module whose invalid logic in its hooks to enable.
* @param string $hook
* The page render hook to assert expected exceptions for.
*/
function assertPageRenderHookExceptions($module, $hook) {
$html_renderer = \Drupal::getContainer()->get('main_content_renderer.html');
// Assert a valid hook implementation doesn't trigger an exception.
$page = [];
$html_renderer->invokePageAttachmentHooks($page);
// Assert that hooks can set cache tags.
$this->assertEqual($page['#cache']['tags'], ['example']);
$this->assertEqual($page['#cache']['contexts'], ['user.permissions']);
// Assert an invalid hook implementation doesn't trigger an exception.
\Drupal::state()->set($module . '.' . $hook . '.descendant_attached', TRUE);
$assertion = $hook . '() implementation that sets #attached on a descendant triggers an exception';
$page = [];
try {
$html_renderer->invokePageAttachmentHooks($page);
$this->error($assertion);
}
catch (\LogicException $e) {
$this->pass($assertion);
$this->assertEqual($e->getMessage(), 'Only #attached and #cache may be set in ' . $hook . '().');
}
\Drupal::state()->set('bc_test.' . $hook . '.descendant_attached', FALSE);
// Assert an invalid hook implementation doesn't trigger an exception.
\Drupal::state()->set('bc_test.' . $hook . '.render_array', TRUE);
$assertion = $hook . '() implementation that sets a child render array triggers an exception';
$page = [];
try {
$html_renderer->invokePageAttachmentHooks($page);
$this->error($assertion);
}
catch (\LogicException $e) {
$this->pass($assertion);
$this->assertEqual($e->getMessage(), 'Only #attached and #cache may be set in ' . $hook . '().');
}
\Drupal::state()->set($module . '.' . $hook . '.render_array', FALSE);
}
}

View file

@ -0,0 +1,253 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\RenderElementTypesTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Url;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the markup of core render element types passed to drupal_render().
*
* @group Common
*/
class RenderElementTypesTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'router_test');
protected function setUp() {
parent::setUp();
$this->installConfig(array('system'));
$this->installSchema('system', array('router'));
\Drupal::service('router.builder')->rebuild();
}
/**
* Asserts that an array of elements is rendered properly.
*
* @param array $elements
* The render element array to test.
* @param string $expected_html
* The expected markup.
* @param string $message
* Assertion message.
*/
protected function assertElements(array $elements, $expected_html, $message) {
$actual_html = \Drupal::service('renderer')->renderRoot($elements);
$out = '<table><tr>';
$out .= '<td valign="top"><pre>' . SafeMarkup::checkPlain($expected_html) . '</pre></td>';
$out .= '<td valign="top"><pre>' . SafeMarkup::checkPlain($actual_html) . '</pre></td>';
$out .= '</tr></table>';
$this->verbose($out);
$this->assertIdentical($actual_html, $expected_html, SafeMarkup::checkPlain($message));
}
/**
* Tests system #type 'container'.
*/
function testContainer() {
// Basic container with no attributes.
$this->assertElements(array(
'#type' => 'container',
'#markup' => 'foo',
), "<div>foo</div>\n", "#type 'container' with no HTML attributes");
// Container with a class.
$this->assertElements(array(
'#type' => 'container',
'#markup' => 'foo',
'#attributes' => array(
'class' => array('bar'),
),
), '<div class="bar">foo</div>' . "\n", "#type 'container' with a class HTML attribute");
// Container with children.
$this->assertElements(array(
'#type' => 'container',
'child' => array(
'#markup' => 'foo',
),
), "<div>foo</div>\n", "#type 'container' with child elements");
}
/**
* Tests system #type 'html_tag'.
*/
function testHtmlTag() {
// Test void element.
$this->assertElements(array(
'#type' => 'html_tag',
'#tag' => 'meta',
'#value' => 'ignored',
'#value_prefix' => 'ignored',
'#value_suffix' => 'ignored',
'#attributes' => array(
'name' => 'description',
'content' => 'Drupal test',
),
), '<meta name="description" content="Drupal test" />' . "\n", "#type 'html_tag', void element renders properly");
// Test non-void element.
$this->assertElements(array(
'#type' => 'html_tag',
'#tag' => 'section',
'#value' => 'value',
'#value_prefix' => 'value_prefix|',
'#value_suffix' => '|value_suffix',
'#attributes' => array(
'class' => array('unicorns'),
),
), '<section class="unicorns">value_prefix|value|value_suffix</section>' . "\n", "#type 'html_tag', non-void element renders properly");
// Test empty void element tag.
$this->assertElements(array(
'#type' => 'html_tag',
'#tag' => 'link',
), "<link />\n", "#type 'html_tag' empty void element renders properly");
// Test empty non-void element tag.
$this->assertElements(array(
'#type' => 'html_tag',
'#tag' => 'section',
), "<section></section>\n", "#type 'html_tag' empty non-void element renders properly");
}
/**
* Tests system #type 'more_link'.
*/
function testMoreLink() {
$elements = array(
array(
'name' => "#type 'more_link' anchor tag generation without extra classes",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromUri('https://www.drupal.org'),
),
'expected' => '//div[@class="more-link"]/a[@href="https://www.drupal.org" and text()="More"]',
),
array(
'name' => "#type 'more_link' anchor tag generation with different link text",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromUri('https://www.drupal.org'),
'#title' => 'More Titles',
),
'expected' => '//div[@class="more-link"]/a[@href="https://www.drupal.org" and text()="More Titles"]',
),
array(
'name' => "#type 'more_link' anchor tag generation with attributes on wrapper",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromUri('https://www.drupal.org'),
'#theme_wrappers' => array(
'container' => array(
'#attributes' => array(
'title' => 'description',
'class' => array('more-link', 'drupal', 'test'),
),
),
),
),
'expected' => '//div[@title="description" and contains(@class, "more-link") and contains(@class, "drupal") and contains(@class, "test")]/a[@href="https://www.drupal.org" and text()="More"]',
),
array(
'name' => "#type 'more_link' anchor tag with a relative path",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromRoute('router_test.1'),
),
'expected' => '//div[@class="more-link"]/a[@href="' . Url::fromRoute('router_test.1')->toString() . '" and text()="More"]',
),
array(
'name' => "#type 'more_link' anchor tag with a route",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromRoute('router_test.1'),
),
'expected' => '//div[@class="more-link"]/a[@href="' . \Drupal::urlGenerator()->generate('router_test.1') . '" and text()="More"]',
),
array(
'name' => "#type 'more_link' anchor tag with an absolute path",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromRoute('system.admin_content'),
'#options' => array('absolute' => TRUE),
),
'expected' => '//div[@class="more-link"]/a[@href="' . Url::fromRoute('system.admin_content')->setAbsolute()->toString() . '" and text()="More"]',
),
array(
'name' => "#type 'more_link' anchor tag to the front page",
'value' => array(
'#type' => 'more_link',
'#url' => Url::fromRoute('<front>'),
),
'expected' => '//div[@class="more-link"]/a[@href="' . Url::fromRoute('<front>')->toString() . '" and text()="More"]',
),
);
foreach($elements as $element) {
$xml = new \SimpleXMLElement(\Drupal::service('renderer')->renderRoot($element['value']));
$result = $xml->xpath($element['expected']);
$this->assertTrue($result, '"' . $element['name'] . '" input rendered correctly by drupal_render().');
}
}
/**
* Tests system #type 'system_compact_link'.
*/
function testSystemCompactLink() {
$elements = array(
array(
'name' => "#type 'system_compact_link' when admin compact mode is off",
'value' => array(
'#type' => 'system_compact_link',
),
'expected' => '//div[@class="compact-link"]/a[contains(@href, "admin/compact/on?") and text()="Hide descriptions"]',
),
array(
'name' => "#type 'system_compact_link' when adding extra attributes",
'value' => array(
'#type' => 'system_compact_link',
'#attributes' => array(
'class' => array('kittens-rule'),
),
),
'expected' => '//div[@class="compact-link"]/a[contains(@href, "admin/compact/on?") and @class="kittens-rule" and text()="Hide descriptions"]',
),
);
foreach ($elements as $element) {
$xml = new \SimpleXMLElement(\Drupal::service('renderer')->renderRoot($element['value']));
$result = $xml->xpath($element['expected']);
$this->assertTrue($result, '"' . $element['name'] . '" is rendered correctly by drupal_render().');
}
// Set admin compact mode on for additional tests.
\Drupal::request()->cookies->set('Drupal_visitor_admin_compact_mode', TRUE);
$element = array(
'name' => "#type 'system_compact_link' when admin compact mode is on",
'value' => array(
'#type' => 'system_compact_link',
),
'expected' => '//div[@class="compact-link"]/a[contains(@href, "admin/compact?") and text()="Show descriptions"]',
);
$xml = new \SimpleXMLElement(\Drupal::service('renderer')->renderRoot($element['value']));
$result = $xml->xpath($element['expected']);
$this->assertTrue($result, '"' . $element['name'] . '" is rendered correctly by drupal_render().');
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\RenderTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\simpletest\KernelTestBase;
/**
* Performs functional tests on drupal_render().
*
* @group Common
*/
class RenderTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'common_test');
/**
* Tests theme preprocess functions being able to attach assets.
*/
function testDrupalRenderThemePreprocessAttached() {
\Drupal::state()->set('theme_preprocess_attached_test', TRUE);
$test_element = [
'#theme' => 'common_test_render_element',
'foo' => [
'#markup' => 'Kittens!',
],
];
\Drupal::service('renderer')->renderRoot($test_element);
$expected_attached = [
'library' => [
'test/generic_preprocess',
'test/specific_preprocess',
]
];
$this->assertEqual($expected_attached, $test_element['#attached'], 'All expected assets from theme preprocess hooks attached.');
\Drupal::state()->set('theme_preprocess_attached_test', FALSE);
}
/**
* Tests drupal_process_attached().
*/
public function testDrupalProcessAttached() {
// Specify invalid attachments in a render array.
$build['#attached']['library'][] = 'core/drupal.states';
$build['#attached']['drupal_process_states'][] = [];
try {
drupal_process_attached($build);
$this->fail("Invalid #attachment 'drupal_process_states' allowed");
}
catch (\Exception $e) {
$this->pass("Invalid #attachment 'drupal_process_states' not allowed");
}
}
}

View file

@ -0,0 +1,162 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\RenderWebTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* Performs integration tests on drupal_render().
*
* @group Common
*/
class RenderWebTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('common_test');
/**
* Tests rendering form elements without passing through
* \Drupal::formBuilder()->doBuildForm().
*/
function testDrupalRenderFormElements() {
// Define a series of form elements.
$element = array(
'#type' => 'button',
'#value' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'submit'));
$element = array(
'#type' => 'textfield',
'#title' => $this->randomMachineName(),
'#value' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'text'));
$element = array(
'#type' => 'password',
'#title' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'password'));
$element = array(
'#type' => 'textarea',
'#title' => $this->randomMachineName(),
'#value' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//textarea');
$element = array(
'#type' => 'radio',
'#title' => $this->randomMachineName(),
'#value' => FALSE,
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'radio'));
$element = array(
'#type' => 'checkbox',
'#title' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'checkbox'));
$element = array(
'#type' => 'select',
'#title' => $this->randomMachineName(),
'#options' => array(
0 => $this->randomMachineName(),
1 => $this->randomMachineName(),
),
);
$this->assertRenderedElement($element, '//select');
$element = array(
'#type' => 'file',
'#title' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'file'));
$element = array(
'#type' => 'item',
'#title' => $this->randomMachineName(),
'#markup' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//div[contains(@class, :class) and contains(., :markup)]/label[contains(., :label)]', array(
':class' => 'js-form-type-item',
':markup' => $element['#markup'],
':label' => $element['#title'],
));
$element = array(
'#type' => 'hidden',
'#title' => $this->randomMachineName(),
'#value' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//input[@type=:type]', array(':type' => 'hidden'));
$element = array(
'#type' => 'link',
'#title' => $this->randomMachineName(),
'#url' => Url::fromRoute('common_test.destination'),
'#options' => array(
'absolute' => TRUE,
),
);
$this->assertRenderedElement($element, '//a[@href=:href and contains(., :title)]', array(
':href' => \Drupal::urlGenerator()->generateFromPath('common-test/destination', ['absolute' => TRUE]),
':title' => $element['#title'],
));
$element = array(
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//details/summary[contains(., :title)]', array(
':title' => $element['#title'],
));
$element = array(
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//details');
$element['item'] = array(
'#type' => 'item',
'#title' => $this->randomMachineName(),
'#markup' => $this->randomMachineName(),
);
$this->assertRenderedElement($element, '//details/div/div[contains(@class, :class) and contains(., :markup)]', array(
':class' => 'js-form-type-item',
':markup' => $element['item']['#markup'],
));
}
/**
* Tests that elements are rendered properly.
*/
protected function assertRenderedElement(array $element, $xpath, array $xpath_args = array()) {
$original_element = $element;
$this->setRawContent(drupal_render_root($element));
$this->verbose('<hr />' . $this->getRawContent());
// @see \Drupal\simpletest\WebTestBase::xpath()
$xpath = $this->buildXPathQuery($xpath, $xpath_args);
$element += array('#value' => NULL);
$this->assertFieldByXPath($xpath, $element['#value'], format_string('#type @type was properly rendered.', array(
'@type' => var_export($element['#type'], TRUE),
)));
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\SimpleTestErrorCollectorTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\simpletest\WebTestBase;
/**
* Tests SimpleTest error and exception collector.
*
* @group Common
*/
class SimpleTestErrorCollectorTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system_test', 'error_test');
/**
* Errors triggered during the test.
*
* Errors are intercepted by the overridden implementation
* of Drupal\simpletest\WebTestBase::error() below.
*
* @var Array
*/
protected $collectedErrors = array();
/**
* Tests that simpletest collects errors from the tested site.
*/
function testErrorCollect() {
$this->collectedErrors = array();
$this->drupalGet('error-test/generate-warnings-with-report');
$this->assertEqual(count($this->collectedErrors), 3, 'Three errors were collected');
if (count($this->collectedErrors) == 3) {
$this->assertError($this->collectedErrors[0], 'Notice', 'Drupal\error_test\Controller\ErrorTestController->generateWarnings()', 'ErrorTestController.php', 'Undefined variable: bananas');
$this->assertError($this->collectedErrors[1], 'Warning', 'Drupal\error_test\Controller\ErrorTestController->generateWarnings()', 'ErrorTestController.php', 'Division by zero');
$this->assertError($this->collectedErrors[2], 'User warning', 'Drupal\error_test\Controller\ErrorTestController->generateWarnings()', 'ErrorTestController.php', 'Drupal is awesome');
}
else {
// Give back the errors to the log report.
foreach ($this->collectedErrors as $error) {
parent::error($error['message'], $error['group'], $error['caller']);
}
}
}
/**
* Stores errors into an array.
*
* This test class is trying to verify that simpletest correctly sees errors
* and warnings. However, it can't generate errors and warnings that
* propagate up to the testing framework itself, or these tests would always
* fail. So, this special copy of error() doesn't propagate the errors up
* the class hierarchy. It just stuffs them into a protected collectedErrors
* array for various assertions to inspect.
*/
protected function error($message = '', $group = 'Other', array $caller = NULL) {
// Due to a WTF elsewhere, simpletest treats debug() and verbose()
// messages as if they were an 'error'. But, we don't want to collect
// those here. This function just wants to collect the real errors (PHP
// notices, PHP fatal errors, etc.), and let all the 'errors' from the
// 'User notice' group bubble up to the parent classes to be handled (and
// eventually displayed) as normal.
if ($group == 'User notice') {
parent::error($message, $group, $caller);
}
// Everything else should be collected but not propagated.
else {
$this->collectedErrors[] = array(
'message' => $message,
'group' => $group,
'caller' => $caller
);
}
}
/**
* Asserts that a collected error matches what we are expecting.
*/
function assertError($error, $group, $function, $file, $message = NULL) {
$this->assertEqual($error['group'], $group, format_string("Group was %group", array('%group' => $group)));
$this->assertEqual($error['caller']['function'], $function, format_string("Function was %function", array('%function' => $function)));
$this->assertEqual(drupal_basename($error['caller']['file']), $file, format_string("File was %file", array('%file' => $file)));
if (isset($message)) {
$this->assertEqual($error['message'], $message, format_string("Message was %message", array('%message' => $message)));
}
}
}

View file

@ -0,0 +1,73 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\SizeUnitTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Utility\Bytes;
use Drupal\simpletest\KernelTestBase;
/**
* Parse a predefined amount of bytes and compare the output with the expected
* value.
*
* @group Common
*/
class SizeUnitTest extends KernelTestBase {
protected $exactTestCases;
protected $roundedTestCases;
protected function setUp() {
parent::setUp();
$kb = Bytes::KILOBYTE;
$this->exactTestCases = array(
'1 byte' => 1,
'1 KB' => $kb,
'1 MB' => $kb * $kb,
'1 GB' => $kb * $kb * $kb,
'1 TB' => $kb * $kb * $kb * $kb,
'1 PB' => $kb * $kb * $kb * $kb * $kb,
'1 EB' => $kb * $kb * $kb * $kb * $kb * $kb,
'1 ZB' => $kb * $kb * $kb * $kb * $kb * $kb * $kb,
'1 YB' => $kb * $kb * $kb * $kb * $kb * $kb * $kb * $kb,
);
$this->roundedTestCases = array(
'2 bytes' => 2,
'1 MB' => ($kb * $kb) - 1, // rounded to 1 MB (not 1000 or 1024 kilobyte!)
round(3623651 / ($this->exactTestCases['1 MB']), 2) . ' MB' => 3623651, // megabytes
round(67234178751368124 / ($this->exactTestCases['1 PB']), 2) . ' PB' => 67234178751368124, // petabytes
round(235346823821125814962843827 / ($this->exactTestCases['1 YB']), 2) . ' YB' => 235346823821125814962843827, // yottabytes
);
}
/**
* Checks that format_size() returns the expected string.
*/
function testCommonFormatSize() {
foreach (array($this->exactTestCases, $this->roundedTestCases) as $test_cases) {
foreach ($test_cases as $expected => $input) {
$this->assertEqual(
($result = format_size($input, NULL)),
$expected,
$expected . ' == ' . $result . ' (' . $input . ' bytes)'
);
}
}
}
/**
* Cross-tests Bytes::toInt() and format_size().
*/
function testCommonParseSizeFormatSize() {
foreach ($this->exactTestCases as $size) {
$this->assertEqual(
$size,
($parsed_size = Bytes::toInt($string = format_size($size, NULL))),
$size . ' == ' . $parsed_size . ' (' . $string . ')'
);
}
}
}

View file

@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\SystemListingTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\simpletest\KernelTestBase;
/**
* Tests scanning system directories in drupal_system_listing().
*
* @group Common
*/
class SystemListingTest extends KernelTestBase {
/**
* Tests that files in different directories take precedence as expected.
*/
function testDirectoryPrecedence() {
// Define the module files we will search for, and the directory precedence
// we expect.
$expected_directories = array(
// When both copies of the module are compatible with Drupal core, the
// copy in the profile directory takes precedence.
'drupal_system_listing_compatible_test' => array(
'core/profiles/testing/modules',
'core/modules/system/tests/modules',
),
);
// This test relies on two versions of the same module existing in
// different places in the filesystem. Without that, the test has no
// meaning, so assert their presence first.
foreach ($expected_directories as $module => $directories) {
foreach ($directories as $directory) {
$filename = "$directory/$module/$module.info.yml";
$this->assertTrue(file_exists(\Drupal::root() . '/' . $filename), format_string('@filename exists.', array('@filename' => $filename)));
}
}
// Now scan the directories and check that the files take precedence as
// expected.
$listing = new ExtensionDiscovery(\Drupal::root());
$listing->setProfileDirectories(array('core/profiles/testing'));
$files = $listing->scan('module');
foreach ($expected_directories as $module => $directories) {
$expected_directory = array_shift($directories);
$expected_uri = "$expected_directory/$module/$module.info.yml";
$this->assertEqual($files[$module]->getPathname(), $expected_uri, format_string('Module @actual was found at @expected.', array(
'@actual' => $files[$module]->getPathname(),
'@expected' => $expected_uri,
)));
}
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\TableSortExtenderUnitTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests table sorting.
*
* @group Common
*/
class TableSortExtenderUnitTest extends KernelTestBase {
/**
* Tests tablesort_init().
*/
function testTableSortInit() {
// Test simple table headers.
$headers = array('foo', 'bar', 'baz');
// Reset $request->query to prevent parameters from Simpletest and Batch API
// ending up in $ts['query'].
$expected_ts = array(
'name' => 'foo',
'sql' => '',
'sort' => 'asc',
'query' => array(),
);
$request = Request::createFromGlobals();
$request->query->replace(array());
\Drupal::getContainer()->get('request_stack')->push($request);
$ts = tablesort_init($headers);
$this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => SafeMarkup::checkPlain(var_export($ts, TRUE)))));
$this->assertEqual($ts, $expected_ts, 'Simple table headers sorted correctly.');
// Test with simple table headers plus $_GET parameters that should _not_
// override the default.
$request = Request::createFromGlobals();
$request->query->replace(array(
// This should not override the table order because only complex
// headers are overridable.
'order' => 'bar',
));
\Drupal::getContainer()->get('request_stack')->push($request);
$ts = tablesort_init($headers);
$this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => SafeMarkup::checkPlain(var_export($ts, TRUE)))));
$this->assertEqual($ts, $expected_ts, 'Simple table headers plus non-overriding $_GET parameters sorted correctly.');
// Test with simple table headers plus $_GET parameters that _should_
// override the default.
$request = Request::createFromGlobals();
$request->query->replace(array(
'sort' => 'DESC',
// Add an unrelated parameter to ensure that tablesort will include
// it in the links that it creates.
'alpha' => 'beta',
));
\Drupal::getContainer()->get('request_stack')->push($request);
$expected_ts['sort'] = 'desc';
$expected_ts['query'] = array('alpha' => 'beta');
$ts = tablesort_init($headers);
$this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => SafeMarkup::checkPlain(var_export($ts, TRUE)))));
$this->assertEqual($ts, $expected_ts, 'Simple table headers plus $_GET parameters sorted correctly.');
// Test complex table headers.
$headers = array(
'foo',
array(
'data' => '1',
'field' => 'one',
'sort' => 'asc',
'colspan' => 1,
),
array(
'data' => '2',
'field' => 'two',
'sort' => 'desc',
),
);
// Reset $_GET from previous assertion.
$request = Request::createFromGlobals();
$request->query->replace(array(
'order' => '2',
));
\Drupal::getContainer()->get('request_stack')->push($request);
$ts = tablesort_init($headers);
$expected_ts = array(
'name' => '2',
'sql' => 'two',
'sort' => 'desc',
'query' => array(),
);
$this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => SafeMarkup::checkPlain(var_export($ts, TRUE)))));
$this->assertEqual($ts, $expected_ts, 'Complex table headers sorted correctly.');
// Test complex table headers plus $_GET parameters that should _not_
// override the default.
$request = Request::createFromGlobals();
$request->query->replace(array(
// This should not override the table order because this header does not
// exist.
'order' => 'bar',
));
\Drupal::getContainer()->get('request_stack')->push($request);
$ts = tablesort_init($headers);
$expected_ts = array(
'name' => '1',
'sql' => 'one',
'sort' => 'asc',
'query' => array(),
);
$this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => SafeMarkup::checkPlain(var_export($ts, TRUE)))));
$this->assertEqual($ts, $expected_ts, 'Complex table headers plus non-overriding $_GET parameters sorted correctly.');
// Test complex table headers plus $_GET parameters that _should_
// override the default.
$request = Request::createFromGlobals();
$request->query->replace(array(
'order' => '1',
'sort' => 'ASC',
// Add an unrelated parameter to ensure that tablesort will include
// it in the links that it creates.
'alpha' => 'beta',
));
\Drupal::getContainer()->get('request_stack')->push($request);
$expected_ts = array(
'name' => '1',
'sql' => 'one',
'sort' => 'asc',
'query' => array('alpha' => 'beta'),
);
$ts = tablesort_init($headers);
$this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => SafeMarkup::checkPlain(var_export($ts, TRUE)))));
$this->assertEqual($ts, $expected_ts, 'Complex table headers plus $_GET parameters sorted correctly.');
}
}

View file

@ -0,0 +1,317 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\UrlTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\Language;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* Confirm that \Drupal\Core\Url,
* \Drupal\Component\Utility\UrlHelper::filterQueryParameters(),
* \Drupal\Component\Utility\UrlHelper::buildQuery(), and _l() work correctly
* with various input.
*
* @group Common
*/
class UrlTest extends WebTestBase {
public static $modules = array('common_test', 'url_alter_test');
/**
* Confirms that invalid URLs are filtered in link generating functions.
*/
function testLinkXSS() {
// Test \Drupal::l().
$text = $this->randomMachineName();
$path = "<SCRIPT>alert('XSS')</SCRIPT>";
$link = \Drupal::l($text, Url::fromUserInput('/' . $path));
$sanitized_path = check_url(Url::fromUri('base:' . $path)->toString());
$this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by _l().', array('@path' => $path)));
// Test \Drupal\Core\Url.
$link = Url::fromUri('base:' . $path)->toString();
$sanitized_path = check_url(Url::fromUri('base:' . $path)->toString());
$this->assertTrue(strpos($link, $sanitized_path) !== FALSE, format_string('XSS attack @path was filtered by #theme', ['@path' => $path]));
}
/**
* Tests that #type=link bubbles outbound route/path processors' cacheability.
*/
function testLinkCacheability() {
$cases = [
['Regular link', 'internal:/user', [], ['contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT]],
['Regular link, absolute', 'internal:/user', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => Cache::PERMANENT]],
['Route processor link', 'route:system.run_cron', [], ['contexts' => [], 'tags' => [], 'max-age' => 0]],
['Route processor link, absolute', 'route:system.run_cron', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => [], 'max-age' => 0]],
['Path processor link', 'internal:/user/1', [], ['contexts' => [], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]],
['Path processor link, absolute', 'internal:/user/1', ['absolute' => TRUE], ['contexts' => ['url.site'], 'tags' => ['user:1'], 'max-age' => Cache::PERMANENT]],
];
foreach ($cases as $case) {
list($title, $uri, $options, $expected_cacheability) = $case;
$expected_cacheability['contexts'] = Cache::mergeContexts($expected_cacheability['contexts'], ['languages:language_interface', 'theme']);
$link = [
'#type' => 'link',
'#title' => $title,
'#options' => $options,
'#url' => Url::fromUri($uri),
];
\Drupal::service('renderer')->renderRoot($link);
$this->pass($title);
$this->assertEqual($expected_cacheability, $link['#cache']);
}
}
/**
* Tests that default and custom attributes are handled correctly on links.
*/
function testLinkAttributes() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
// Test that hreflang is added when a link has a known language.
$language = new Language(array('id' => 'fr', 'name' => 'French'));
$hreflang_link = array(
'#type' => 'link',
'#options' => array(
'language' => $language,
),
'#url' => Url::fromUri('https://www.drupal.org'),
'#title' => 'bar',
);
$langcode = $language->getId();
// Test that the default hreflang handling for links does not override a
// hreflang attribute explicitly set in the render array.
$hreflang_override_link = $hreflang_link;
$hreflang_override_link['#options']['attributes']['hreflang'] = 'foo';
$rendered = $renderer->renderRoot($hreflang_link);
$this->assertTrue($this->hasAttribute('hreflang', $rendered, $langcode), format_string('hreflang attribute with value @langcode is present on a rendered link when langcode is provided in the render array.', array('@langcode' => $langcode)));
$rendered = $renderer->renderRoot($hreflang_override_link);
$this->assertTrue($this->hasAttribute('hreflang', $rendered, 'foo'), format_string('hreflang attribute with value @hreflang is present on a rendered link when @hreflang is provided in the render array.', array('@hreflang' => 'foo')));
// Test the active class in links produced by _l() and #type 'link'.
$options_no_query = array();
$options_query = array(
'query' => array(
'foo' => 'bar',
'one' => 'two',
),
);
$options_query_reverse = array(
'query' => array(
'one' => 'two',
'foo' => 'bar',
),
);
// Test #type link.
$path = 'common-test/type-link-active-class';
$this->drupalGet($path, $options_no_query);
$links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_no_query)->toString(), ':class' => 'is-active'));
$this->assertTrue(isset($links[0]), 'A link generated by _l() to the current page is marked active.');
$links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_query)->toString(), ':class' => 'is-active'));
$this->assertTrue(isset($links[0]), 'A link generated by _l() to the current page with a query string when the current page has no query string is not marked active.');
$this->drupalGet($path, $options_query);
$links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_query)->toString(), ':class' => 'is-active'));
$this->assertTrue(isset($links[0]), 'A link generated by _l() to the current page with a query string that matches the current query string is marked active.');
$links = $this->xpath('//a[@href = :href and contains(@class, :class)]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_query_reverse)->toString(), ':class' => 'is-active'));
$this->assertTrue(isset($links[0]), 'A link generated by _l() to the current page with a query string that has matching parameters to the current query string but in a different order is marked active.');
$links = $this->xpath('//a[@href = :href and not(contains(@class, :class))]', array(':href' => Url::fromRoute('common_test.l_active_class', [], $options_no_query)->toString(), ':class' => 'is-active'));
$this->assertTrue(isset($links[0]), 'A link generated by _l() to the current page without a query string when the current page has a query string is not marked active.');
// Test adding a custom class in links produced by _l() and #type 'link'.
// Test _l().
$class_l = $this->randomMachineName();
$link_l = \Drupal::l($this->randomMachineName(), new Url('<current>', [], ['attributes' => ['class' => [$class_l]]]));
$this->assertTrue($this->hasAttribute('class', $link_l, $class_l), format_string('Custom class @class is present on link when requested by l()', array('@class' => $class_l)));
// Test #type.
$class_theme = $this->randomMachineName();
$type_link = array(
'#type' => 'link',
'#title' => $this->randomMachineName(),
'#url' => Url::fromRoute('<current>'),
'#options' => array(
'attributes' => array(
'class' => array($class_theme),
),
),
);
$link_theme = $renderer->renderRoot($type_link);
$this->assertTrue($this->hasAttribute('class', $link_theme, $class_theme), format_string('Custom class @class is present on link when requested by #type', array('@class' => $class_theme)));
}
/**
* Tests that link functions support render arrays as 'text'.
*/
function testLinkRenderArrayText() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
// Build a link with _l() for reference.
$l = \Drupal::l('foo', Url::fromUri('https://www.drupal.org'));
// Test a renderable array passed to _l().
$renderable_text = array('#markup' => 'foo');
$l_renderable_text = \Drupal::l($renderable_text, Url::fromUri('https://www.drupal.org'));
$this->assertEqual($l_renderable_text, $l);
// Test a themed link with plain text 'text'.
$type_link_plain_array = array(
'#type' => 'link',
'#title' => 'foo',
'#url' => Url::fromUri('https://www.drupal.org'),
);
$type_link_plain = $renderer->renderRoot($type_link_plain_array);
$this->assertEqual($type_link_plain, $l);
// Build a themed link with renderable 'text'.
$type_link_nested_array = array(
'#type' => 'link',
'#title' => array('#markup' => 'foo'),
'#url' => Url::fromUri('https://www.drupal.org'),
);
$type_link_nested = $renderer->renderRoot($type_link_nested_array);
$this->assertEqual($type_link_nested, $l);
}
/**
* Checks for class existence in link.
*
* @param $link
* URL to search.
* @param $class
* Element class to search for.
*
* @return bool
* TRUE if the class is found, FALSE otherwise.
*/
private function hasAttribute($attribute, $link, $class) {
return preg_match('|' . $attribute . '="([^\"\s]+\s+)*' . $class . '|', $link);
}
/**
* Tests UrlHelper::filterQueryParameters().
*/
function testDrupalGetQueryParameters() {
$original = array(
'a' => 1,
'b' => array(
'd' => 4,
'e' => array(
'f' => 5,
),
),
'c' => 3,
);
// First-level exclusion.
$result = $original;
unset($result['b']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, array('b')), $result, "'b' was removed.");
// Second-level exclusion.
$result = $original;
unset($result['b']['d']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, array('b[d]')), $result, "'b[d]' was removed.");
// Third-level exclusion.
$result = $original;
unset($result['b']['e']['f']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, array('b[e][f]')), $result, "'b[e][f]' was removed.");
// Multiple exclusions.
$result = $original;
unset($result['a'], $result['b']['e'], $result['c']);
$this->assertEqual(UrlHelper::filterQueryParameters($original, array('a', 'b[e]', 'c')), $result, "'a', 'b[e]', 'c' were removed.");
}
/**
* Tests UrlHelper::parse().
*/
function testDrupalParseUrl() {
// Relative, absolute, and external URLs, without/with explicit script path,
// without/with Drupal path.
foreach (array('', '/', 'https://www.drupal.org/') as $absolute) {
foreach (array('', 'index.php/') as $script) {
foreach (array('', 'foo/bar') as $path) {
$url = $absolute . $script . $path . '?foo=bar&bar=baz&baz#foo';
$expected = array(
'path' => $absolute . $script . $path,
'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''),
'fragment' => 'foo',
);
$this->assertEqual(UrlHelper::parse($url), $expected, 'URL parsed correctly.');
}
}
}
// Relative URL that is known to confuse parse_url().
$url = 'foo/bar:1';
$result = array(
'path' => 'foo/bar:1',
'query' => array(),
'fragment' => '',
);
$this->assertEqual(UrlHelper::parse($url), $result, 'Relative URL parsed correctly.');
// Test that drupal can recognize an absolute URL. Used to prevent attack vectors.
$url = 'https://www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo';
$this->assertTrue(UrlHelper::isExternal($url), 'Correctly identified an external URL.');
// Test that UrlHelper::parse() does not allow spoofing a URL to force a malicious redirect.
$parts = UrlHelper::parse('forged:http://cwe.mitre.org/data/definitions/601.html');
$this->assertFalse(UrlHelper::isValid($parts['path'], TRUE), '\Drupal\Component\Utility\UrlHelper::isValid() correctly parsed a forged URL.');
}
/**
* Tests external URL handling.
*/
function testExternalUrls() {
$test_url = 'https://www.drupal.org/';
// Verify external URL can contain a fragment.
$url = $test_url . '#drupal';
$result = Url::fromUri($url)->toString();
$this->assertEqual($url, $result, 'External URL with fragment works without a fragment in $options.');
// Verify fragment can be overridden in an external URL.
$url = $test_url . '#drupal';
$fragment = $this->randomMachineName(10);
$result = Url::fromUri($url, array('fragment' => $fragment))->toString();
$this->assertEqual($test_url . '#' . $fragment, $result, 'External URL fragment is overridden with a custom fragment in $options.');
// Verify external URL can contain a query string.
$url = $test_url . '?drupal=awesome';
$result = Url::fromUri($url)->toString();
$this->assertEqual($url, $result);
// Verify external URL can be extended with a query string.
$url = $test_url;
$query = array($this->randomMachineName(5) => $this->randomMachineName(5));
$result = Url::fromUri($url, array('query' => $query))->toString();
$this->assertEqual($url . '?' . http_build_query($query, '', '&'), $result, 'External URL can be extended with a query string in $options.');
// Verify query string can be extended in an external URL.
$url = $test_url . '?drupal=awesome';
$query = array($this->randomMachineName(5) => $this->randomMachineName(5));
$result = Url::fromUri($url, array('query' => $query))->toString();
$this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result);
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Common\XssUnitTest.
*/
namespace Drupal\system\Tests\Common;
use Drupal\Component\Utility\UrlHelper;
use Drupal\simpletest\KernelTestBase;
/**
* Confirm that \Drupal\Component\Utility\Xss::filter() and check_url() work
* correctly, including invalid multi-byte sequences.
*
* @group Common
*/
class XssUnitTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'system');
protected function setUp() {
parent::setUp();
$this->installConfig(array('system'));
}
/**
* Tests t() functionality.
*/
function testT() {
$text = t('Simple text');
$this->assertEqual($text, 'Simple text', 't leaves simple text alone.');
$text = t('Escaped text: @value', array('@value' => '<script>'));
$this->assertEqual($text, 'Escaped text: &lt;script&gt;', 't replaces and escapes string.');
$text = t('Placeholder text: %value', array('%value' => '<script>'));
$this->assertEqual($text, 'Placeholder text: <em class="placeholder">&lt;script&gt;</em>', 't replaces, escapes and themes string.');
$text = t('Verbatim text: !value', array('!value' => '<script>'));
$this->assertEqual($text, 'Verbatim text: <script>', 't replaces verbatim string as-is.');
}
/**
* Checks that harmful protocols are stripped.
*/
function testBadProtocolStripping() {
// Ensure that check_url() strips out harmful protocols, and encodes for
// HTML.
// Ensure \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() can
// be used to return a plain-text string stripped of harmful protocols.
$url = 'javascript:http://www.example.com/?x=1&y=2';
$expected_plain = 'http://www.example.com/?x=1&y=2';
$expected_html = 'http://www.example.com/?x=1&amp;y=2';
$this->assertIdentical(check_url($url), $expected_html, 'check_url() filters a URL and encodes it for HTML.');
$this->assertIdentical(UrlHelper::stripDangerousProtocols($url), $expected_plain, '\Drupal\Component\Utility\Url::stripDangerousProtocols() filters a URL and returns plain text.');
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Condition\ConditionFormTest.
*/
namespace Drupal\system\Tests\Condition;
use Drupal\simpletest\WebTestBase;
/**
* Tests that condition plugins basic form handling is working.
*
* Checks condition forms and submission and gives a very cursory check to make
* sure the configuration that was submitted actually causes the condition to
* validate correctly.
*
* @group Condition
*/
class ConditionFormTest extends WebTestBase {
public static $modules = array('node', 'condition_test');
/**
* Submit the condition_node_type_test_form to test condition forms.
*/
function testConfigForm() {
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Page'));
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
$article = entity_create('node', array('type' => 'article', 'title' => $this->randomMachineName()));
$article->save();
$this->drupalGet('condition_test');
$this->assertField('bundles[article]', 'There is an article bundle selector.');
$this->assertField('bundles[page]', 'There is a page bundle selector.');
$this->drupalPostForm(NULL, array('bundles[page]' => 'page', 'bundles[article]' => 'article'), t('Submit'));
// @see \Drupal\condition_test\FormController::submitForm()
$this->assertText('Bundle: page');
$this->assertText('Bundle: article');
$this->assertText('Executed successfully.', 'The form configured condition executed properly.');
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Condition\CurrentThemeConditionTest.
*/
namespace Drupal\system\Tests\Condition;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the CurrentThemeCondition plugin.
*
* @group Condition
*/
class CurrentThemeConditionTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('system', 'theme_test');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', array('router'));
}
/**
* Tests the current theme condition.
*/
public function testCurrentTheme() {
\Drupal::service('theme_handler')->install(array('test_theme'));
$manager = \Drupal::service('plugin.manager.condition');
/** @var $condition \Drupal\Core\Condition\ConditionInterface */
$condition = $manager->createInstance('current_theme');
$condition->setConfiguration(array('theme' => 'test_theme'));
/** @var $condition_negated \Drupal\Core\Condition\ConditionInterface */
$condition_negated = $manager->createInstance('current_theme');
$condition_negated->setConfiguration(array('theme' => 'test_theme', 'negate' => TRUE));
$this->assertEqual($condition->summary(), SafeMarkup::format('The current theme is @theme', array('@theme' => 'test_theme')));
$this->assertEqual($condition_negated->summary(), SafeMarkup::format('The current theme is not @theme', array('@theme' => 'test_theme')));
// The expected theme has not been set up yet.
$this->assertFalse($condition->execute());
$this->assertTrue($condition_negated->execute());
// Set the expected theme to be used.
$this->config('system.theme')->set('default', 'test_theme')->save();
\Drupal::theme()->resetActiveTheme();
$this->assertTrue($condition->execute());
$this->assertFalse($condition_negated->execute());
}
}

View file

@ -0,0 +1,155 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\AlterTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the hook_query_alter capabilities of the Select builder.
*
* @group Database
* @see database_test_query_alter()
*/
class AlterTest extends DatabaseTestBase {
/**
* Tests that we can do basic alters.
*/
function testSimpleAlter() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('database_test_alter_add_range');
$result = $query->execute()->fetchAll();
$this->assertEqual(count($result), 2, 'Returned the correct number of rows.');
}
/**
* Tests that we can alter the joins on a query.
*/
function testAlterWithJoin() {
$query = db_select('test_task');
$tid_field = $query->addField('test_task', 'tid');
$task_field = $query->addField('test_task', 'task');
$query->orderBy($task_field);
$query->addTag('database_test_alter_add_join');
$result = $query->execute();
$records = $result->fetchAll();
$this->assertEqual(count($records), 2, 'Returned the correct number of rows.');
$this->assertEqual($records[0]->name, 'George', 'Correct data retrieved.');
$this->assertEqual($records[0]->$tid_field, 4, 'Correct data retrieved.');
$this->assertEqual($records[0]->$task_field, 'sing', 'Correct data retrieved.');
$this->assertEqual($records[1]->name, 'George', 'Correct data retrieved.');
$this->assertEqual($records[1]->$tid_field, 5, 'Correct data retrieved.');
$this->assertEqual($records[1]->$task_field, 'sleep', 'Correct data retrieved.');
}
/**
* Tests that we can alter a query's conditionals.
*/
function testAlterChangeConditional() {
$query = db_select('test_task');
$tid_field = $query->addField('test_task', 'tid');
$pid_field = $query->addField('test_task', 'pid');
$task_field = $query->addField('test_task', 'task');
$people_alias = $query->join('test', 'people', "test_task.pid = people.id");
$name_field = $query->addField($people_alias, 'name', 'name');
$query->condition('test_task.tid', '1');
$query->orderBy($tid_field);
$query->addTag('database_test_alter_change_conditional');
$result = $query->execute();
$records = $result->fetchAll();
$this->assertEqual(count($records), 1, 'Returned the correct number of rows.');
$this->assertEqual($records[0]->$name_field, 'John', 'Correct data retrieved.');
$this->assertEqual($records[0]->$tid_field, 2, 'Correct data retrieved.');
$this->assertEqual($records[0]->$pid_field, 1, 'Correct data retrieved.');
$this->assertEqual($records[0]->$task_field, 'sleep', 'Correct data retrieved.');
}
/**
* Tests that we can alter the fields of a query.
*/
function testAlterChangeFields() {
$query = db_select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$query->orderBy('name');
$query->addTag('database_test_alter_change_fields');
$record = $query->execute()->fetch();
$this->assertEqual($record->$name_field, 'George', 'Correct data retrieved.');
$this->assertFalse(isset($record->$age_field), 'Age field not found, as intended.');
}
/**
* Tests that we can alter expressions in the query.
*/
function testAlterExpression() {
$query = db_select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addExpression("age*2", 'double_age');
$query->condition('age', 27);
$query->addTag('database_test_alter_change_expressions');
$result = $query->execute();
// Ensure that we got the right record.
$record = $result->fetch();
$this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.');
$this->assertEqual($record->$age_field, 27*3, 'Fetched age expression is correct.');
}
/**
* Tests that we can remove a range() value from a query.
*
* This also tests hook_query_TAG_alter().
*/
function testAlterRemoveRange() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->range(0, 2);
$query->addTag('database_test_alter_remove_range');
$num_records = count($query->execute()->fetchAll());
$this->assertEqual($num_records, 4, 'Returned the correct number of rows.');
}
/**
* Tests that we can do basic alters on subqueries.
*/
function testSimpleAlterSubquery() {
// Create a sub-query with an alter tag.
$subquery = db_select('test', 'p');
$subquery->addField('p', 'name');
$subquery->addField('p', 'id');
// Pick out George.
$subquery->condition('age', 27);
$subquery->addExpression("age*2", 'double_age');
// This query alter should change it to age * 3.
$subquery->addTag('database_test_alter_change_expressions');
// Create a main query and join to sub-query.
$query = db_select('test_task', 'tt');
$query->join($subquery, 'pq', 'pq.id = tt.pid');
$age_field = $query->addField('pq', 'double_age');
$name_field = $query->addField('pq', 'name');
$record = $query->execute()->fetch();
$this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.');
$this->assertEqual($record->$age_field, 27*3, 'Fetched age expression is correct.');
}
}

View file

@ -0,0 +1,141 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\BasicSyntaxTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests SQL syntax interpretation.
*
* In order to ensure consistent SQL handling throughout Drupal
* across multiple kinds of database systems, we test that the
* database system interprets SQL syntax in an expected fashion.
*
* @group Database
*/
class BasicSyntaxTest extends DatabaseTestBase {
/**
* Tests string concatenation.
*/
function testConcatLiterals() {
$result = db_query('SELECT CONCAT(:a1, CONCAT(:a2, CONCAT(:a3, CONCAT(:a4, :a5))))', array(
':a1' => 'This',
':a2' => ' ',
':a3' => 'is',
':a4' => ' a ',
':a5' => 'test.',
));
$this->assertIdentical($result->fetchField(), 'This is a test.', 'Basic CONCAT works.');
}
/**
* Tests string concatenation with field values.
*/
function testConcatFields() {
$result = db_query('SELECT CONCAT(:a1, CONCAT(name, CONCAT(:a2, CONCAT(age, :a3)))) FROM {test} WHERE age = :age', array(
':a1' => 'The age of ',
':a2' => ' is ',
':a3' => '.',
':age' => 25,
));
$this->assertIdentical($result->fetchField(), 'The age of John is 25.', 'Field CONCAT works.');
}
/**
* Tests string concatenation with separator.
*/
function testConcatWsLiterals() {
$result = db_query("SELECT CONCAT_WS(', ', :a1, NULL, :a2, :a3, :a4)", array(
':a1' => 'Hello',
':a2' => NULL,
':a3' => '',
':a4' => 'world.',
));
$this->assertIdentical($result->fetchField(), 'Hello, , world.');
}
/**
* Tests string concatenation with separator, with field values.
*/
function testConcatWsFields() {
$result = db_query("SELECT CONCAT_WS('-', :a1, name, :a2, age) FROM {test} WHERE age = :age", array(
':a1' => 'name',
':a2' => 'age',
':age' => 25,
));
$this->assertIdentical($result->fetchField(), 'name-John-age-25');
}
/**
* Tests escaping of LIKE wildcards.
*/
function testLikeEscape() {
db_insert('test')
->fields(array(
'name' => 'Ring_',
))
->execute();
// Match both "Ringo" and "Ring_".
$num_matches = db_select('test', 't')
->condition('name', 'Ring_', 'LIKE')
->countQuery()
->execute()
->fetchField();
$this->assertIdentical($num_matches, '2', 'Found 2 records.');
// Match only "Ring_" using a LIKE expression with no wildcards.
$num_matches = db_select('test', 't')
->condition('name', db_like('Ring_'), 'LIKE')
->countQuery()
->execute()
->fetchField();
$this->assertIdentical($num_matches, '1', 'Found 1 record.');
}
/**
* Tests a LIKE query containing a backslash.
*/
function testLikeBackslash() {
db_insert('test')
->fields(array('name'))
->values(array(
'name' => 'abcde\f',
))
->values(array(
'name' => 'abc%\_',
))
->execute();
// Match both rows using a LIKE expression with two wildcards and a verbatim
// backslash.
$num_matches = db_select('test', 't')
->condition('name', 'abc%\\\\_', 'LIKE')
->countQuery()
->execute()
->fetchField();
$this->assertIdentical($num_matches, '2', 'Found 2 records.');
// Match only the former using a LIKE expression with no wildcards.
$num_matches = db_select('test', 't')
->condition('name', db_like('abc%\_'), 'LIKE')
->countQuery()
->execute()
->fetchField();
$this->assertIdentical($num_matches, '1', 'Found 1 record.');
}
/**
* Tests \Drupal\Core\Database\Connection::getFullQualifiedTableName().
*/
public function testGetFullQualifiedTableName() {
$database = \Drupal::database();
$num_matches = $database->select($database->getFullQualifiedTableName('test'), 't')
->countQuery()
->execute()
->fetchField();
$this->assertIdentical($num_matches, '4', 'Found 4 records.');
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\CaseSensitivityTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests handling case sensitive collation.
*
* @group Database
*/
class CaseSensitivityTest extends DatabaseTestBase {
/**
* Tests BINARY collation in MySQL.
*/
function testCaseSensitiveInsert() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
db_insert('test')
->fields(array(
'name' => 'john', // <- A record already exists with name 'John'.
'age' => 2,
'job' => 'Baby',
))
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$this->assertIdentical($num_records_before + 1, (int) $num_records_after, 'Record inserts correctly.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'john'))->fetchField();
$this->assertIdentical($saved_age, '2', 'Can retrieve after inserting.');
}
}

View file

@ -0,0 +1,141 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\ConnectionTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Database;
/**
* Tests of the core database system.
*
* @group Database
*/
class ConnectionTest extends DatabaseTestBase {
/**
* Tests that connections return appropriate connection objects.
*/
function testConnectionRouting() {
// Clone the primary credentials to a replica connection.
// Note this will result in two independent connection objects that happen
// to point to the same place.
$connection_info = Database::getConnectionInfo('default');
Database::addConnectionInfo('default', 'replica', $connection_info['default']);
$db1 = Database::getConnection('default', 'default');
$db2 = Database::getConnection('replica', 'default');
$this->assertNotNull($db1, 'default connection is a real connection object.');
$this->assertNotNull($db2, 'replica connection is a real connection object.');
$this->assertNotIdentical($db1, $db2, 'Each target refers to a different connection.');
// Try to open those targets another time, that should return the same objects.
$db1b = Database::getConnection('default', 'default');
$db2b = Database::getConnection('replica', 'default');
$this->assertIdentical($db1, $db1b, 'A second call to getConnection() returns the same object.');
$this->assertIdentical($db2, $db2b, 'A second call to getConnection() returns the same object.');
// Try to open an unknown target.
$unknown_target = $this->randomMachineName();
$db3 = Database::getConnection($unknown_target, 'default');
$this->assertNotNull($db3, 'Opening an unknown target returns a real connection object.');
$this->assertIdentical($db1, $db3, 'An unknown target opens the default connection.');
// Try to open that unknown target another time, that should return the same object.
$db3b = Database::getConnection($unknown_target, 'default');
$this->assertIdentical($db3, $db3b, 'A second call to getConnection() returns the same object.');
}
/**
* Tests that connections return appropriate connection objects.
*/
function testConnectionRoutingOverride() {
// Clone the primary credentials to a replica connection.
// Note this will result in two independent connection objects that happen
// to point to the same place.
$connection_info = Database::getConnectionInfo('default');
Database::addConnectionInfo('default', 'replica', $connection_info['default']);
Database::ignoreTarget('default', 'replica');
$db1 = Database::getConnection('default', 'default');
$db2 = Database::getConnection('replica', 'default');
$this->assertIdentical($db1, $db2, 'Both targets refer to the same connection.');
}
/**
* Tests the closing of a database connection.
*/
function testConnectionClosing() {
// Open the default target so we have an object to compare.
$db1 = Database::getConnection('default', 'default');
// Try to close the default connection, then open a new one.
Database::closeConnection('default', 'default');
$db2 = Database::getConnection('default', 'default');
// Opening a connection after closing it should yield an object different than the original.
$this->assertNotIdentical($db1, $db2, 'Opening the default connection after it is closed returns a new object.');
}
/**
* Tests the connection options of the active database.
*/
function testConnectionOptions() {
$connection_info = Database::getConnectionInfo('default');
// Be sure we're connected to the default database.
$db = Database::getConnection('default', 'default');
$connectionOptions = $db->getConnectionOptions();
// In the MySQL driver, the port can be different, so check individual
// options.
$this->assertEqual($connection_info['default']['driver'], $connectionOptions['driver'], 'The default connection info driver matches the current connection options driver.');
$this->assertEqual($connection_info['default']['database'], $connectionOptions['database'], 'The default connection info database matches the current connection options database.');
// Set up identical replica and confirm connection options are identical.
Database::addConnectionInfo('default', 'replica', $connection_info['default']);
$db2 = Database::getConnection('replica', 'default');
$connectionOptions2 = $db2->getConnectionOptions();
// Get a fresh copy of the default connection options.
$connectionOptions = $db->getConnectionOptions();
$this->assertIdentical($connectionOptions, $connectionOptions2, 'The default and replica connection options are identical.');
// Set up a new connection with different connection info.
$test = $connection_info['default'];
$test['database'] .= 'test';
Database::addConnectionInfo('test', 'default', $test);
$connection_info = Database::getConnectionInfo('test');
// Get a fresh copy of the default connection options.
$connectionOptions = $db->getConnectionOptions();
$this->assertNotEqual($connection_info['default']['database'], $connectionOptions['database'], 'The test connection info database does not match the current connection options database.');
}
/**
* Ensure that you cannot execute multiple statements on phpversion() > 5.5.21 or > 5.6.5.
*/
public function testMultipleStatementsForNewPhp() {
// This just tests mysql, as other PDO integrations don't allow to disable
// multiple statements.
if (Database::getConnection()->databaseType() !== 'mysql' || !defined('\PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
return;
}
$db = Database::getConnection('default', 'default');
try {
$db->query('SELECT * FROM {test}; SELECT * FROM {test_people}')->execute();
$this->fail('NO PDO exception thrown for multiple statements.');
}
catch (\Exception $e) {
$this->pass('PDO exception thrown for multiple statements.');
}
}
}

View file

@ -0,0 +1,250 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\ConnectionUnitTest.
*/
namespace Drupal\system\Tests\Database;
use Doctrine\Common\Reflection\StaticReflectionProperty;
use Drupal\Core\Database\Database;
use Drupal\Core\Site\Settings;
use Drupal\simpletest\KernelTestBase;
/**
* Tests management of database connections.
*
* @group Database
*/
class ConnectionUnitTest extends KernelTestBase {
protected $key;
protected $target;
protected $monitor;
protected $originalCount;
protected function setUp() {
parent::setUp();
$this->key = 'default';
$this->originalTarget = 'default';
$this->target = 'DatabaseConnectionUnitTest';
// Determine whether the database driver is MySQL. If it is not, the test
// methods will not be executed.
// @todo Make this test driver-agnostic, or find a proper way to skip it.
// See https://www.drupal.org/node/1273478.
$connection_info = Database::getConnectionInfo('default');
$this->skipTest = (bool) ($connection_info['default']['driver'] != 'mysql');
if ($this->skipTest) {
// Insert an assertion to prevent Simpletest from interpreting the test
// as failure.
$this->pass('This test is only compatible with MySQL.');
}
// Create an additional connection to monitor the connections being opened
// and closed in this test.
// @see TestBase::changeDatabasePrefix()
Database::addConnectionInfo('default', 'monitor', $connection_info['default']);
$this->monitor = Database::getConnection('monitor');
}
/**
* Adds a new database connection info to Database.
*/
protected function addConnection() {
// Add a new target to the connection, by cloning the current connection.
$connection_info = Database::getConnectionInfo($this->key);
Database::addConnectionInfo($this->key, $this->target, $connection_info[$this->originalTarget]);
// Verify that the new target exists.
$info = Database::getConnectionInfo($this->key);
// Note: Custom assertion message to not expose database credentials.
$this->assertIdentical($info[$this->target], $connection_info[$this->key], 'New connection info found.');
}
/**
* Returns the connection ID of the current test connection.
*
* @return integer
*/
protected function getConnectionID() {
return (int) Database::getConnection($this->target, $this->key)->query('SELECT CONNECTION_ID()')->fetchField();
}
/**
* Asserts that a connection ID exists.
*
* @param integer $id
* The connection ID to verify.
*/
protected function assertConnection($id) {
$list = $this->monitor->query('SHOW PROCESSLIST')->fetchAllKeyed(0, 0);
return $this->assertTrue(isset($list[$id]), format_string('Connection ID @id found.', array('@id' => $id)));
}
/**
* Asserts that a connection ID does not exist.
*
* @param integer $id
* The connection ID to verify.
*/
protected function assertNoConnection($id) {
$list = $this->monitor->query('SHOW PROCESSLIST')->fetchAllKeyed(0, 0);
return $this->assertFalse(isset($list[$id]), format_string('Connection ID @id not found.', array('@id' => $id)));
}
/**
* Tests Database::closeConnection() without query.
*
* @todo getConnectionID() executes a query.
*/
function testOpenClose() {
if ($this->skipTest) {
return;
}
// Add and open a new connection.
$this->addConnection();
$id = $this->getConnectionID();
Database::getConnection($this->target, $this->key);
// Verify that there is a new connection.
$this->assertConnection($id);
// Close the connection.
Database::closeConnection($this->target, $this->key);
// Wait 20ms to give the database engine sufficient time to react.
usleep(20000);
// Verify that we are back to the original connection count.
$this->assertNoConnection($id);
}
/**
* Tests Database::closeConnection() with a query.
*/
function testOpenQueryClose() {
if ($this->skipTest) {
return;
}
// Add and open a new connection.
$this->addConnection();
$id = $this->getConnectionID();
Database::getConnection($this->target, $this->key);
// Verify that there is a new connection.
$this->assertConnection($id);
// Execute a query.
Database::getConnection($this->target, $this->key)->query('SHOW TABLES');
// Close the connection.
Database::closeConnection($this->target, $this->key);
// Wait 20ms to give the database engine sufficient time to react.
usleep(20000);
// Verify that we are back to the original connection count.
$this->assertNoConnection($id);
}
/**
* Tests Database::closeConnection() with a query and custom prefetch method.
*/
function testOpenQueryPrefetchClose() {
if ($this->skipTest) {
return;
}
// Add and open a new connection.
$this->addConnection();
$id = $this->getConnectionID();
Database::getConnection($this->target, $this->key);
// Verify that there is a new connection.
$this->assertConnection($id);
// Execute a query.
Database::getConnection($this->target, $this->key)->query('SHOW TABLES')->fetchCol();
// Close the connection.
Database::closeConnection($this->target, $this->key);
// Wait 20ms to give the database engine sufficient time to react.
usleep(20000);
// Verify that we are back to the original connection count.
$this->assertNoConnection($id);
}
/**
* Tests Database::closeConnection() with a select query.
*/
function testOpenSelectQueryClose() {
if ($this->skipTest) {
return;
}
// Add and open a new connection.
$this->addConnection();
$id = $this->getConnectionID();
Database::getConnection($this->target, $this->key);
// Verify that there is a new connection.
$this->assertConnection($id);
// Create a table.
$name = 'foo';
Database::getConnection($this->target, $this->key)->schema()->createTable($name, array(
'fields' => array(
'name' => array(
'type' => 'varchar',
'length' => 255,
),
),
));
// Execute a query.
Database::getConnection($this->target, $this->key)->select('foo', 'f')
->fields('f', array('name'))
->execute()
->fetchAll();
// Drop the table.
Database::getConnection($this->target, $this->key)->schema()->dropTable($name);
// Close the connection.
Database::closeConnection($this->target, $this->key);
// Wait 20ms to give the database engine sufficient time to react.
usleep(20000);
// Verify that we are back to the original connection count.
$this->assertNoConnection($id);
}
/**
* Tests pdo options override.
*/
public function testConnectionOpen() {
$connection = Database::getConnection('default');
$reflection = new \ReflectionObject($connection);
$connection_property = $reflection->getProperty('connection');
$connection_property->setAccessible(TRUE);
$error_mode = $connection_property->getValue($connection)
->getAttribute(\PDO::ATTR_ERRMODE);
$this->assertEqual($error_mode, \PDO::ERRMODE_EXCEPTION, 'Ensure the default error mode is set to exception.');
$connection = Database::getConnectionInfo('default');
$connection['default']['pdo'][\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_SILENT;
Database::addConnectionInfo('test', 'default', $connection['default']);
$connection = Database::getConnection('default', 'test');
$reflection = new \ReflectionObject($connection);
$connection_property = $reflection->getProperty('connection');
$connection_property->setAccessible(TRUE);
$error_mode = $connection_property->getValue($connection)
->getAttribute(\PDO::ATTR_ERRMODE);
$this->assertEqual($error_mode, \PDO::ERRMODE_SILENT, 'Ensure PDO connection options can be overridden.');
Database::removeConnection('test');
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\DatabaseExceptionWrapperTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\Database;
use Drupal\simpletest\KernelTestBase;
/**
* Tests exceptions thrown by queries.
*
* @group Database
*/
class DatabaseExceptionWrapperTest extends KernelTestBase {
/**
* Tests the expected database exception thrown for prepared statements.
*/
public function testPreparedStatement() {
$connection = Database::getConnection();
try {
// SQLite validates the syntax upon preparing a statement already.
// @throws \PDOException
$query = $connection->prepare('bananas');
// MySQL only validates the syntax upon trying to execute a query.
// @throws \Drupal\Core\Database\DatabaseExceptionWrapper
$connection->query($query);
$this->fail('Expected PDOException or DatabaseExceptionWrapper, none was thrown.');
}
catch (\PDOException $e) {
$this->pass('Expected PDOException was thrown.');
}
catch (DatabaseExceptionWrapper $e) {
$this->pass('Expected DatabaseExceptionWrapper was thrown.');
}
catch (\Exception $e) {
$this->fail("Thrown exception is not a PDOException:\n" . (string) $e);
}
}
/**
* Tests the expected database exception thrown for inexistent tables.
*/
public function testQueryThrowsDatabaseExceptionWrapperException() {
$connection = Database::getConnection();
try {
$connection->query('SELECT * FROM {does_not_exist}');
$this->fail('Expected PDOException, none was thrown.');
}
catch (DatabaseExceptionWrapper $e) {
$this->pass('Expected DatabaseExceptionWrapper was thrown.');
}
catch (\Exception $e) {
$this->fail("Thrown exception is not a DatabaseExceptionWrapper:\n" . (string) $e);
}
}
}

View file

@ -0,0 +1,142 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\DatabaseTestBase.
*/
namespace Drupal\system\Tests\Database;
use Drupal\simpletest\KernelTestBase;
/**
* Base class for databases database tests.
*
* Because all database tests share the same test data, we can centralize that
* here.
*/
abstract class DatabaseTestBase extends KernelTestBase {
public static $modules = array('database_test');
protected function setUp() {
parent::setUp();
$this->installSchema('database_test', array(
'test',
'test_people',
'test_people_copy',
'test_one_blob',
'test_two_blobs',
'test_task',
'test_null',
'test_serialized',
));
self::addSampleData();
}
/**
* Sets up tables for NULL handling.
*/
function ensureSampleDataNull() {
db_insert('test_null')
->fields(array('name', 'age'))
->values(array(
'name' => 'Kermit',
'age' => 25,
))
->values(array(
'name' => 'Fozzie',
'age' => NULL,
))
->values(array(
'name' => 'Gonzo',
'age' => 27,
))
->execute();
}
/**
* Sets up our sample data.
*/
static function addSampleData() {
// We need the IDs, so we can't use a multi-insert here.
$john = db_insert('test')
->fields(array(
'name' => 'John',
'age' => 25,
'job' => 'Singer',
))
->execute();
$george = db_insert('test')
->fields(array(
'name' => 'George',
'age' => 27,
'job' => 'Singer',
))
->execute();
db_insert('test')
->fields(array(
'name' => 'Ringo',
'age' => 28,
'job' => 'Drummer',
))
->execute();
$paul = db_insert('test')
->fields(array(
'name' => 'Paul',
'age' => 26,
'job' => 'Songwriter',
))
->execute();
db_insert('test_people')
->fields(array(
'name' => 'Meredith',
'age' => 30,
'job' => 'Speaker',
))
->execute();
db_insert('test_task')
->fields(array('pid', 'task', 'priority'))
->values(array(
'pid' => $john,
'task' => 'eat',
'priority' => 3,
))
->values(array(
'pid' => $john,
'task' => 'sleep',
'priority' => 4,
))
->values(array(
'pid' => $john,
'task' => 'code',
'priority' => 1,
))
->values(array(
'pid' => $george,
'task' => 'sing',
'priority' => 2,
))
->values(array(
'pid' => $george,
'task' => 'sleep',
'priority' => 2,
))
->values(array(
'pid' => $paul,
'task' => 'found new band',
'priority' => 1,
))
->values(array(
'pid' => $paul,
'task' => 'perform at superbowl',
'priority' => 3,
))
->execute();
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\DatabaseWebTestBase.
*/
namespace Drupal\system\Tests\Database;
use Drupal\simpletest\WebTestBase;
/**
* Base class for databases database tests.
*/
abstract class DatabaseWebTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('database_test');
protected function setUp() {
parent::setUp();
DatabaseTestBase::addSampleData();
}
}

View file

@ -0,0 +1,73 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\DeleteTruncateTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests delete and truncate queries.
*
* The DELETE tests are not as extensive, as all of the interesting code for
* DELETE queries is in the conditional which is identical to the UPDATE and
* SELECT conditional handling.
*
* The TRUNCATE tests are not extensive either, because the behavior of
* TRUNCATE queries is not consistent across database engines. We only test
* that a TRUNCATE query actually deletes all rows from the target table.
*
* @group Database
*/
class DeleteTruncateTest extends DatabaseTestBase {
/**
* Confirms that we can use a subselect in a delete successfully.
*/
function testSubselectDelete() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_task}')->fetchField();
$pid_to_delete = db_query("SELECT * FROM {test_task} WHERE task = 'sleep'")->fetchField();
$subquery = db_select('test', 't')
->fields('t', array('id'))
->condition('t.id', array($pid_to_delete), 'IN');
$delete = db_delete('test_task')
->condition('task', 'sleep')
->condition('pid', $subquery, 'IN');
$num_deleted = $delete->execute();
$this->assertEqual($num_deleted, 1, 'Deleted 1 record.');
$num_records_after = db_query('SELECT COUNT(*) FROM {test_task}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.');
}
/**
* Confirms that we can delete a single record successfully.
*/
function testSimpleDelete() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$num_deleted = db_delete('test')
->condition('id', 1)
->execute();
$this->assertIdentical($num_deleted, 1, 'Deleted 1 record.');
$num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.');
}
/**
* Confirms that we can truncate a whole table successfully.
*/
function testTruncate() {
$num_records_before = db_query("SELECT COUNT(*) FROM {test}")->fetchField();
$this->assertTrue($num_records_before > 0, 'The table is not empty.');
db_truncate('test')->execute();
$num_records_after = db_query("SELECT COUNT(*) FROM {test}")->fetchField();
$this->assertEqual(0, $num_records_after, 'Truncate really deletes everything.');
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\FakeRecord.
*/
namespace Drupal\system\Tests\Database;
/**
* Fetches into a class.
*
* PDO supports using a new instance of an arbitrary class for records
* rather than just a stdClass or array. This class is for testing that
* functionality. (See testQueryFetchClass() below)
*/
class FakeRecord { }

View file

@ -0,0 +1,150 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\FetchTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\RowCountException;
use Drupal\Core\Database\StatementInterface;
/**
* Tests the Database system's various fetch capabilities.
*
* We get timeout errors if we try to run too many tests at once.
*
* @group Database
*/
class FetchTest extends DatabaseTestBase {
/**
* Confirms that we can fetch a record properly in default object mode.
*/
function testQueryFetchDefault() {
$records = array();
$result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25));
$this->assertTrue($result instanceof StatementInterface, 'Result set is a Drupal statement object.');
foreach ($result as $record) {
$records[] = $record;
$this->assertTrue(is_object($record), 'Record is an object.');
$this->assertIdentical($record->name, 'John', '25 year old is John.');
}
$this->assertIdentical(count($records), 1, 'There is only one record.');
}
/**
* Confirms that we can fetch a record to an object explicitly.
*/
function testQueryFetchObject() {
$records = array();
$result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => \PDO::FETCH_OBJ));
foreach ($result as $record) {
$records[] = $record;
$this->assertTrue(is_object($record), 'Record is an object.');
$this->assertIdentical($record->name, 'John', '25 year old is John.');
}
$this->assertIdentical(count($records), 1, 'There is only one record.');
}
/**
* Confirms that we can fetch a record to an associative array explicitly.
*/
function testQueryFetchArray() {
$records = array();
$result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => \PDO::FETCH_ASSOC));
foreach ($result as $record) {
$records[] = $record;
if ($this->assertTrue(is_array($record), 'Record is an array.')) {
$this->assertIdentical($record['name'], 'John', 'Record can be accessed associatively.');
}
}
$this->assertIdentical(count($records), 1, 'There is only one record.');
}
/**
* Confirms that we can fetch a record into a new instance of a custom class.
*
* @see \Drupal\system\Tests\Database\FakeRecord
*/
function testQueryFetchClass() {
$records = array();
$result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => 'Drupal\system\Tests\Database\FakeRecord'));
foreach ($result as $record) {
$records[] = $record;
if ($this->assertTrue($record instanceof FakeRecord, 'Record is an object of class FakeRecord.')) {
$this->assertIdentical($record->name, 'John', '25 year old is John.');
}
}
$this->assertIdentical(count($records), 1, 'There is only one record.');
}
/**
* Confirms that we can fetch a record into an indexed array explicitly.
*/
function testQueryFetchNum() {
$records = array();
$result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => \PDO::FETCH_NUM));
foreach ($result as $record) {
$records[] = $record;
if ($this->assertTrue(is_array($record), 'Record is an array.')) {
$this->assertIdentical($record[0], 'John', 'Record can be accessed numerically.');
}
}
$this->assertIdentical(count($records), 1, 'There is only one record');
}
/**
* Confirms that we can fetch a record into a doubly-keyed array explicitly.
*/
function testQueryFetchBoth() {
$records = array();
$result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25), array('fetch' => \PDO::FETCH_BOTH));
foreach ($result as $record) {
$records[] = $record;
if ($this->assertTrue(is_array($record), 'Record is an array.')) {
$this->assertIdentical($record[0], 'John', 'Record can be accessed numerically.');
$this->assertIdentical($record['name'], 'John', 'Record can be accessed associatively.');
}
}
$this->assertIdentical(count($records), 1, 'There is only one record.');
}
/**
* Confirms that we can fetch an entire column of a result set at once.
*/
function testQueryFetchCol() {
$result = db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25));
$column = $result->fetchCol();
$this->assertIdentical(count($column), 3, 'fetchCol() returns the right number of records.');
$result = db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25));
$i = 0;
foreach ($result as $record) {
$this->assertIdentical($record->name, $column[$i++], 'Column matches direct access.');
}
}
/**
* Tests that rowCount() throws exception on SELECT query.
*/
public function testRowCount() {
$result = db_query('SELECT name FROM {test}');
try {
$result->rowCount();
$exception = FALSE;
}
catch (RowCountException $e) {
$exception = TRUE;
}
$this->assertTrue($exception, 'Exception was thrown');
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\InsertDefaultsTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Query\NoFieldsException;
/**
* Tests the Insert query builder with default values.
*
* @group Database
*/
class InsertDefaultsTest extends DatabaseTestBase {
/**
* Tests that we can run a query that uses default values for everything.
*/
function testDefaultInsert() {
$query = db_insert('test')->useDefaults(array('job'));
$id = $query->execute();
$schema = drupal_get_module_schema('database_test', 'test');
$job = db_query('SELECT job FROM {test} WHERE id = :id', array(':id' => $id))->fetchField();
$this->assertEqual($job, $schema['fields']['job']['default'], 'Default field value is set.');
}
/**
* Tests that no action will be preformed if no fields are specified.
*/
function testDefaultEmptyInsert() {
$num_records_before = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField();
try {
db_insert('test')->execute();
// This is only executed if no exception has been thrown.
$this->fail('Expected exception NoFieldsException has not been thrown.');
} catch (NoFieldsException $e) {
$this->pass('Expected exception NoFieldsException has been thrown.');
}
$num_records_after = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$this->assertIdentical($num_records_before, $num_records_after, 'Do nothing as no fields are specified.');
}
/**
* Tests that we can insert fields with values and defaults in the same query.
*/
function testDefaultInsertWithFields() {
$query = db_insert('test')
->fields(array('name' => 'Bob'))
->useDefaults(array('job'));
$id = $query->execute();
$schema = drupal_get_module_schema('database_test', 'test');
$job = db_query('SELECT job FROM {test} WHERE id = :id', array(':id' => $id))->fetchField();
$this->assertEqual($job, $schema['fields']['job']['default'], 'Default field value is set.');
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\InsertLobTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the Insert query builder with LOB fields.
*
* @group Database
*/
class InsertLobTest extends DatabaseTestBase {
/**
* Tests that we can insert a single blob field successfully.
*/
function testInsertOneBlob() {
$data = "This is\000a test.";
$this->assertTrue(strlen($data) === 15, 'Test data contains a NULL.');
$id = db_insert('test_one_blob')
->fields(array('blob1' => $data))
->execute();
$r = db_query('SELECT * FROM {test_one_blob} WHERE id = :id', array(':id' => $id))->fetchAssoc();
$this->assertTrue($r['blob1'] === $data, format_string('Can insert a blob: id @id, @data.', array('@id' => $id, '@data' => serialize($r))));
}
/**
* Tests that we can insert multiple blob fields in the same query.
*/
function testInsertMultipleBlob() {
$id = db_insert('test_two_blobs')
->fields(array(
'blob1' => 'This is',
'blob2' => 'a test',
))
->execute();
$r = db_query('SELECT * FROM {test_two_blobs} WHERE id = :id', array(':id' => $id))->fetchAssoc();
$this->assertTrue($r['blob1'] === 'This is' && $r['blob2'] === 'a test', 'Can insert multiple blobs per row.');
}
}

View file

@ -0,0 +1,185 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\InsertTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the insert builder.
*
* @group Database
*/
class InsertTest extends DatabaseTestBase {
/**
* Tests very basic insert functionality.
*/
function testSimpleInsert() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$query = db_insert('test');
$query->fields(array(
'name' => 'Yoko',
'age' => '29',
));
$query->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$this->assertIdentical($num_records_before + 1, (int) $num_records_after, 'Record inserts correctly.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Yoko'))->fetchField();
$this->assertIdentical($saved_age, '29', 'Can retrieve after inserting.');
}
/**
* Tests that we can insert multiple records in one query object.
*/
function testMultiInsert() {
$num_records_before = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$query = db_insert('test');
$query->fields(array(
'name' => 'Larry',
'age' => '30',
));
// We should be able to specify values in any order if named.
$query->values(array(
'age' => '31',
'name' => 'Curly',
));
// We should be able to say "use the field order".
// This is not the recommended mechanism for most cases, but it should work.
$query->values(array('Moe', '32'));
$query->execute();
$num_records_after = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$this->assertIdentical($num_records_before + 3, $num_records_after, 'Record inserts correctly.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Larry'))->fetchField();
$this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Curly'))->fetchField();
$this->assertIdentical($saved_age, '31', 'Can retrieve after inserting.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Moe'))->fetchField();
$this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.');
}
/**
* Tests that an insert object can be reused with new data after it executes.
*/
function testRepeatedInsert() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$query = db_insert('test');
$query->fields(array(
'name' => 'Larry',
'age' => '30',
));
$query->execute(); // This should run the insert, but leave the fields intact.
// We should be able to specify values in any order if named.
$query->values(array(
'age' => '31',
'name' => 'Curly',
));
$query->execute();
// We should be able to say "use the field order".
$query->values(array('Moe', '32'));
$query->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$this->assertIdentical((int) $num_records_before + 3, (int) $num_records_after, 'Record inserts correctly.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Larry'))->fetchField();
$this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Curly'))->fetchField();
$this->assertIdentical($saved_age, '31', 'Can retrieve after inserting.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Moe'))->fetchField();
$this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.');
}
/**
* Tests that we can specify fields without values and specify values later.
*/
function testInsertFieldOnlyDefinition() {
// This is useful for importers, when we want to create a query and define
// its fields once, then loop over a multi-insert execution.
db_insert('test')
->fields(array('name', 'age'))
->values(array('Larry', '30'))
->values(array('Curly', '31'))
->values(array('Moe', '32'))
->execute();
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Larry'))->fetchField();
$this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Curly'))->fetchField();
$this->assertIdentical($saved_age, '31', 'Can retrieve after inserting.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Moe'))->fetchField();
$this->assertIdentical($saved_age, '32', 'Can retrieve after inserting.');
}
/**
* Tests that inserts return the proper auto-increment ID.
*/
function testInsertLastInsertID() {
$id = db_insert('test')
->fields(array(
'name' => 'Larry',
'age' => '30',
))
->execute();
$this->assertIdentical($id, '5', 'Auto-increment ID returned successfully.');
}
/**
* Tests that the INSERT INTO ... SELECT (fields) ... syntax works.
*/
function testInsertSelectFields() {
$query = db_select('test_people', 'tp');
// The query builder will always append expressions after fields.
// Add the expression first to test that the insert fields are correctly
// re-ordered.
$query->addExpression('tp.age', 'age');
$query
->fields('tp', array('name','job'))
->condition('tp.name', 'Meredith');
// The resulting query should be equivalent to:
// INSERT INTO test (age, name, job)
// SELECT tp.age AS age, tp.name AS name, tp.job AS job
// FROM test_people tp
// WHERE tp.name = 'Meredith'
db_insert('test')
->from($query)
->execute();
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Meredith'))->fetchField();
$this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.');
}
/**
* Tests that the INSERT INTO ... SELECT * ... syntax works.
*/
function testInsertSelectAll() {
$query = db_select('test_people', 'tp')
->fields('tp')
->condition('tp.name', 'Meredith');
// The resulting query should be equivalent to:
// INSERT INTO test_people_copy
// SELECT *
// FROM test_people tp
// WHERE tp.name = 'Meredith'
db_insert('test_people_copy')
->from($query)
->execute();
$saved_age = db_query('SELECT age FROM {test_people_copy} WHERE name = :name', array(':name' => 'Meredith'))->fetchField();
$this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.');
}
}

View file

@ -0,0 +1,73 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\InvalidDataTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\IntegrityConstraintViolationException;
/**
* Tests handling of some invalid data.
*
* @group Database
*/
class InvalidDataTest extends DatabaseTestBase {
/**
* Tests aborting of traditional SQL database systems with invalid data.
*/
function testInsertDuplicateData() {
// Try to insert multiple records where at least one has bad data.
try {
db_insert('test')
->fields(array('name', 'age', 'job'))
->values(array(
'name' => 'Elvis',
'age' => 63,
'job' => 'Singer',
))->values(array(
'name' => 'John', // <-- Duplicate value on unique field.
'age' => 17,
'job' => 'Consultant',
))
->values(array(
'name' => 'Frank',
'age' => 75,
'job' => 'Singer',
))
->execute();
$this->fail('Insert succeeded when it should not have.');
}
catch (IntegrityConstraintViolationException $e) {
// Check if the first record was inserted.
$name = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 63))->fetchField();
if ($name == 'Elvis') {
if (!Database::getConnection()->supportsTransactions()) {
// This is an expected fail.
// Database engines that don't support transactions can leave partial
// inserts in place when an error occurs. This is the case for MySQL
// when running on a MyISAM table.
$this->pass("The whole transaction has not been rolled-back when a duplicate key insert occurs, this is expected because the database doesn't support transactions");
}
else {
$this->fail('The whole transaction is rolled back when a duplicate key insert occurs.');
}
}
else {
$this->pass('The whole transaction is rolled back when a duplicate key insert occurs.');
}
// Ensure the other values were not inserted.
$record = db_select('test')
->fields('test', array('name', 'age'))
->condition('age', array(17, 75), 'IN')
->execute()->fetchObject();
$this->assertFalse($record, 'The rest of the insert aborted as expected.');
}
}
}

View file

@ -0,0 +1,133 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\LoggingTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Database;
/**
* Tests the query logging facility.
*
* @group Database
*/
class LoggingTest extends DatabaseTestBase {
/**
* Tests that we can log the existence of a query.
*/
function testEnableLogging() {
Database::startLog('testing');
db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25))->fetchCol();
db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchCol();
// Trigger a call that does not have file in the backtrace.
call_user_func_array('db_query', array('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo')))->fetchCol();
$queries = Database::getLog('testing', 'default');
$this->assertEqual(count($queries), 3, 'Correct number of queries recorded.');
foreach ($queries as $query) {
$this->assertEqual($query['caller']['function'], __FUNCTION__, 'Correct function in query log.');
}
}
/**
* Tests that we can run two logs in parallel.
*/
function testEnableMultiLogging() {
Database::startLog('testing1');
db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25))->fetchCol();
Database::startLog('testing2');
db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchCol();
$queries1 = Database::getLog('testing1');
$queries2 = Database::getLog('testing2');
$this->assertEqual(count($queries1), 2, 'Correct number of queries recorded for log 1.');
$this->assertEqual(count($queries2), 1, 'Correct number of queries recorded for log 2.');
}
/**
* Tests logging queries against multiple targets on the same connection.
*/
function testEnableTargetLogging() {
// Clone the primary credentials to a replica connection and to another fake
// connection.
$connection_info = Database::getConnectionInfo('default');
Database::addConnectionInfo('default', 'replica', $connection_info['default']);
Database::startLog('testing1');
db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25))->fetchCol();
db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'), array('target' => 'replica'));//->fetchCol();
$queries1 = Database::getLog('testing1');
$this->assertEqual(count($queries1), 2, 'Recorded queries from all targets.');
$this->assertEqual($queries1[0]['target'], 'default', 'First query used default target.');
$this->assertEqual($queries1[1]['target'], 'replica', 'Second query used replica target.');
}
/**
* Tests that logs to separate targets use the same connection properly.
*
* This test is identical to the one above, except that it doesn't create
* a fake target so the query should fall back to running on the default
* target.
*/
function testEnableTargetLoggingNoTarget() {
Database::startLog('testing1');
db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25))->fetchCol();
// We use "fake" here as a target because any non-existent target will do.
// However, because all of the tests in this class share a single page
// request there is likely to be a target of "replica" from one of the other
// unit tests, so we use a target here that we know with absolute certainty
// does not exist.
db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'), array('target' => 'fake'))->fetchCol();
$queries1 = Database::getLog('testing1');
$this->assertEqual(count($queries1), 2, 'Recorded queries from all targets.');
$this->assertEqual($queries1[0]['target'], 'default', 'First query used default target.');
$this->assertEqual($queries1[1]['target'], 'default', 'Second query used default target as fallback.');
}
/**
* Tests that we can log queries separately on different connections.
*/
function testEnableMultiConnectionLogging() {
// Clone the primary credentials to a fake connection.
// That both connections point to the same physical database is irrelevant.
$connection_info = Database::getConnectionInfo('default');
Database::addConnectionInfo('test2', 'default', $connection_info['default']);
Database::startLog('testing1');
Database::startLog('testing1', 'test2');
db_query('SELECT name FROM {test} WHERE age > :age', array(':age' => 25))->fetchCol();
$old_key = db_set_active('test2');
db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'), array('target' => 'replica'))->fetchCol();
db_set_active($old_key);
$queries1 = Database::getLog('testing1');
$queries2 = Database::getLog('testing1', 'test2');
$this->assertEqual(count($queries1), 1, 'Correct number of queries recorded for first connection.');
$this->assertEqual(count($queries2), 1, 'Correct number of queries recorded for second connection.');
}
}

View file

@ -0,0 +1,237 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\MergeTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Query\Merge;
use Drupal\Core\Database\Query\InvalidMergeQueryException;
/**
* Tests the MERGE query builder.
*
* @group Database
*/
class MergeTest extends DatabaseTestBase {
/**
* Confirms that we can merge-insert a record successfully.
*/
function testMergeInsert() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$result = db_merge('test_people')
->key('job', 'Presenter')
->fields(array(
'age' => 31,
'name' => 'Tiffany',
))
->execute();
$this->assertEqual($result, Merge::STATUS_INSERT, 'Insert status returned.');
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before + 1, $num_records_after, 'Merge inserted properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Presenter'))->fetch();
$this->assertEqual($person->name, 'Tiffany', 'Name set correctly.');
$this->assertEqual($person->age, 31, 'Age set correctly.');
$this->assertEqual($person->job, 'Presenter', 'Job set correctly.');
}
/**
* Confirms that we can merge-update a record successfully.
*/
function testMergeUpdate() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$result = db_merge('test_people')
->key('job', 'Speaker')
->fields(array(
'age' => 31,
'name' => 'Tiffany',
))
->execute();
$this->assertEqual($result, Merge::STATUS_UPDATE, 'Update status returned.');
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
$this->assertEqual($person->name, 'Tiffany', 'Name set correctly.');
$this->assertEqual($person->age, 31, 'Age set correctly.');
$this->assertEqual($person->job, 'Speaker', 'Job set correctly.');
}
/**
* Confirms that we can merge-update a record successfully.
*
* This test varies from the previous test because it manually defines which
* fields are inserted, and which fields are updated.
*/
function testMergeUpdateExcept() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
db_merge('test_people')
->key('job', 'Speaker')
->insertFields(array('age' => 31))
->updateFields(array('name' => 'Tiffany'))
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
$this->assertEqual($person->name, 'Tiffany', 'Name set correctly.');
$this->assertEqual($person->age, 30, 'Age skipped correctly.');
$this->assertEqual($person->job, 'Speaker', 'Job set correctly.');
}
/**
* Confirms that we can merge-update a record, with alternate replacement.
*/
function testMergeUpdateExplicit() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
db_merge('test_people')
->key('job', 'Speaker')
->insertFields(array(
'age' => 31,
'name' => 'Tiffany',
))
->updateFields(array(
'name' => 'Joe',
))
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
$this->assertEqual($person->name, 'Joe', 'Name set correctly.');
$this->assertEqual($person->age, 30, 'Age skipped correctly.');
$this->assertEqual($person->job, 'Speaker', 'Job set correctly.');
}
/**
* Confirms that we can merge-update a record successfully, with expressions.
*/
function testMergeUpdateExpression() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$age_before = db_query('SELECT age FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetchField();
// This is a very contrived example, as I have no idea why you'd want to
// change age this way, but that's beside the point.
// Note that we are also double-setting age here, once as a literal and
// once as an expression. This test will only pass if the expression wins,
// which is what is supposed to happen.
db_merge('test_people')
->key('job', 'Speaker')
->fields(array('name' => 'Tiffany'))
->insertFields(array('age' => 31))
->expression('age', 'age + :age', array(':age' => 4))
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
$this->assertEqual($person->name, 'Tiffany', 'Name set correctly.');
$this->assertEqual($person->age, $age_before + 4, 'Age updated correctly.');
$this->assertEqual($person->job, 'Speaker', 'Job set correctly.');
}
/**
* Tests that we can merge-insert without any update fields.
*/
function testMergeInsertWithoutUpdate() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
db_merge('test_people')
->key('job', 'Presenter')
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before + 1, $num_records_after, 'Merge inserted properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Presenter'))->fetch();
$this->assertEqual($person->name, '', 'Name set correctly.');
$this->assertEqual($person->age, 0, 'Age set correctly.');
$this->assertEqual($person->job, 'Presenter', 'Job set correctly.');
}
/**
* Confirms that we can merge-update without any update fields.
*/
function testMergeUpdateWithoutUpdate() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
db_merge('test_people')
->key('job', 'Speaker')
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after, 'Merge skipped properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
$this->assertEqual($person->name, 'Meredith', 'Name skipped correctly.');
$this->assertEqual($person->age, 30, 'Age skipped correctly.');
$this->assertEqual($person->job, 'Speaker', 'Job skipped correctly.');
db_merge('test_people')
->key('job', 'Speaker')
->insertFields(array('age' => 31))
->execute();
$num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after, 'Merge skipped properly.');
$person = db_query('SELECT * FROM {test_people} WHERE job = :job', array(':job' => 'Speaker'))->fetch();
$this->assertEqual($person->name, 'Meredith', 'Name skipped correctly.');
$this->assertEqual($person->age, 30, 'Age skipped correctly.');
$this->assertEqual($person->job, 'Speaker', 'Job skipped correctly.');
}
/**
* Tests that an invalid merge query throws an exception.
*/
function testInvalidMerge() {
try {
// This query will fail because there is no key field specified.
// Normally it would throw an exception but we are suppressing it with
// the throw_exception option.
$options['throw_exception'] = FALSE;
db_merge('test_people', $options)
->fields(array(
'age' => 31,
'name' => 'Tiffany',
))
->execute();
$this->pass('$options[\'throw_exception\'] is FALSE, no InvalidMergeQueryException thrown.');
}
catch (InvalidMergeQueryException $e) {
$this->fail('$options[\'throw_exception\'] is FALSE, but InvalidMergeQueryException thrown for invalid query.');
return;
}
try {
// This query will fail because there is no key field specified.
db_merge('test_people')
->fields(array(
'age' => 31,
'name' => 'Tiffany',
))
->execute();
}
catch (InvalidMergeQueryException $e) {
$this->pass('InvalidMergeQueryException thrown for invalid query.');
return;
}
$this->fail('No InvalidMergeQueryException thrown');
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\NextIdTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the sequences API.
*
* @group Database
*/
class NextIdTest extends KernelTestBase {
/**
* The modules to enable.
* @var array
*/
public static $modules = array('system');
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'sequences');
}
/**
* Tests that the sequences API works.
*/
function testDbNextId() {
$first = db_next_id();
$second = db_next_id();
// We can test for exact increase in here because we know there is no
// other process operating on these tables -- normally we could only
// expect $second > $first.
$this->assertEqual($first + 1, $second, 'The second call from a sequence provides a number increased by one.');
$result = db_next_id(1000);
$this->assertEqual($result, 1001, 'Sequence provides a larger number than the existing ID.');
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\QueryTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests Drupal's extended prepared statement syntax..
*
* @group Database
*/
class QueryTest extends DatabaseTestBase {
/**
* Tests that we can pass an array of values directly in the query.
*/
function testArraySubstitution() {
$names = db_query('SELECT name FROM {test} WHERE age IN ( :ages[] ) ORDER BY age', array(':ages[]' => array(25, 26, 27)))->fetchAll();
$this->assertEqual(count($names), 3, 'Correct number of names returned');
$names = db_query('SELECT name FROM {test} WHERE age IN ( :ages[] ) ORDER BY age', array(':ages[]' => array(25)))->fetchAll();
$this->assertEqual(count($names), 1, 'Correct number of names returned');
}
/**
* Tests that we can not pass a scalar value when an array is expected.
*/
function testScalarSubstitution() {
try {
$names = db_query('SELECT name FROM {test} WHERE age IN ( :ages[] ) ORDER BY age', array(':ages[]' => 25))->fetchAll();
$this->fail('Array placeholder with scalar argument should result in an exception.');
}
catch (\InvalidArgumentException $e) {
$this->pass('Array placeholder with scalar argument should result in an exception.');
}
}
/**
* Tests SQL injection via database query array arguments.
*/
public function testArrayArgumentsSQLInjection() {
// Attempt SQL injection and verify that it does not work.
$condition = array(
"1 ;INSERT INTO {test} (name) VALUES ('test12345678'); -- " => '',
'1' => '',
);
try {
db_query("SELECT * FROM {test} WHERE name = :name", array(':name' => $condition))->fetchObject();
$this->fail('SQL injection attempt via array arguments should result in a database exception.');
}
catch (\InvalidArgumentException $e) {
$this->pass('SQL injection attempt via array arguments should result in a database exception.');
}
// Test that the insert query that was used in the SQL injection attempt did
// not result in a row being inserted in the database.
$result = db_select('test')
->condition('name', 'test12345678')
->countQuery()
->execute()
->fetchField();
$this->assertFalse($result, 'SQL injection attempt did not result in a row being inserted in the database table.');
}
/**
* Tests numeric query parameter expansion in expressions.
*
* @see \Drupal\Core\Database\Driver\sqlite\Statement::getStatement()
* @see http://bugs.php.net/bug.php?id=45259
*/
public function testNumericExpressionSubstitution() {
$count = db_query('SELECT COUNT(*) >= 3 FROM {test}')->fetchField();
$this->assertEqual((bool) $count, TRUE);
$count = db_query('SELECT COUNT(*) >= :count FROM {test}', array(
':count' => 3,
))->fetchField();
$this->assertEqual((bool) $count, TRUE);
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\RangeQueryTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the Range query functionality.
*
* @group Database
*/
class RangeQueryTest extends DatabaseTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('database_test');
/**
* Confirms that range queries work and return the correct result.
*/
function testRangeQuery() {
// Test if return correct number of rows.
$range_rows = db_query_range("SELECT name FROM {test} ORDER BY name", 1, 3)->fetchAll();
$this->assertEqual(count($range_rows), 3, 'Range query work and return correct number of rows.');
// Test if return target data.
$raw_rows = db_query('SELECT name FROM {test} ORDER BY name')->fetchAll();
$raw_rows = array_slice($raw_rows, 1, 3);
$this->assertEqual($range_rows, $raw_rows);
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\RegressionTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Regression tests cases for the database layer.
*
* @group Database
*/
class RegressionTest extends DatabaseTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'user');
/**
* Ensures that non-ASCII UTF-8 data is stored in the database properly.
*/
function testRegression_310447() {
// That's a 255 character UTF-8 string.
$job = str_repeat("é", 255);
db_insert('test')
->fields(array(
'name' => $this->randomMachineName(),
'age' => 20,
'job' => $job,
))->execute();
$from_database = db_query('SELECT job FROM {test} WHERE job = :job', array(':job' => $job))->fetchField();
$this->assertIdentical($job, $from_database, 'The database handles UTF-8 characters cleanly.');
}
/**
* Tests the db_table_exists() function.
*/
function testDBTableExists() {
$this->assertIdentical(TRUE, db_table_exists('test'), 'Returns true for existent table.');
$this->assertIdentical(FALSE, db_table_exists('nosuchtable'), 'Returns false for nonexistent table.');
}
/**
* Tests the db_field_exists() function.
*/
function testDBFieldExists() {
$this->assertIdentical(TRUE, db_field_exists('test', 'name'), 'Returns true for existent column.');
$this->assertIdentical(FALSE, db_field_exists('test', 'nosuchcolumn'), 'Returns false for nonexistent column.');
}
/**
* Tests the db_index_exists() function.
*/
function testDBIndexExists() {
$this->assertIdentical(TRUE, db_index_exists('test', 'ages'), 'Returns true for existent index.');
$this->assertIdentical(FALSE, db_index_exists('test', 'nosuchindex'), 'Returns false for nonexistent index.');
}
}

View file

@ -0,0 +1,660 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SchemaTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\simpletest\KernelTestBase;
use Drupal\Component\Utility\Unicode;
/**
* Tests table creation and modification via the schema API.
*
* @group Database
*/
class SchemaTest extends KernelTestBase {
/**
* A global counter for table and field creation.
*/
protected $counter;
/**
* Tests database interactions.
*/
function testSchema() {
// Try creating a table.
$table_specification = array(
'description' => 'Schema table description may contain "quotes" and could be long—very long indeed.',
'fields' => array(
'id' => array(
'type' => 'int',
'default' => NULL,
),
'test_field' => array(
'type' => 'int',
'not null' => TRUE,
'description' => 'Schema table description may contain "quotes" and could be long—very long indeed. There could be "multiple quoted regions".',
),
'test_field_string' => array(
'type' => 'varchar',
'length' => 20,
'not null' => TRUE,
'default' => "'\"funky default'\"",
'description' => 'Schema column description for string.',
),
'test_field_string_ascii' => array(
'type' => 'varchar_ascii',
'length' => 255,
'description' => 'Schema column description for ASCII string.',
),
),
);
db_create_table('test_table', $table_specification);
// Assert that the table exists.
$this->assertTrue(db_table_exists('test_table'), 'The table exists.');
// Assert that the table comment has been set.
$this->checkSchemaComment($table_specification['description'], 'test_table');
// Assert that the column comment has been set.
$this->checkSchemaComment($table_specification['fields']['test_field']['description'], 'test_table', 'test_field');
if (Database::getConnection()->databaseType() == 'mysql') {
// Make sure that varchar fields have the correct collation.
$columns = db_query('SHOW FULL COLUMNS FROM {test_table}');
foreach ($columns as $column) {
if ($column->Field == 'test_field_string') {
$string_check = ($column->Collation == 'utf8mb4_general_ci');
}
if ($column->Field == 'test_field_string_ascii') {
$string_ascii_check = ($column->Collation == 'ascii_general_ci');
}
}
$this->assertTrue(!empty($string_check), 'string field has the right collation.');
$this->assertTrue(!empty($string_ascii_check), 'ASCII string field has the right collation.');
}
// An insert without a value for the column 'test_table' should fail.
$this->assertFalse($this->tryInsert(), 'Insert without a default failed.');
// Add a default value to the column.
db_field_set_default('test_table', 'test_field', 0);
// The insert should now succeed.
$this->assertTrue($this->tryInsert(), 'Insert with a default succeeded.');
// Remove the default.
db_field_set_no_default('test_table', 'test_field');
// The insert should fail again.
$this->assertFalse($this->tryInsert(), 'Insert without a default failed.');
// Test for fake index and test for the boolean result of indexExists().
$index_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
$this->assertIdentical($index_exists, FALSE, 'Fake index does not exists');
// Add index.
db_add_index('test_table', 'test_field', array('test_field'));
// Test for created index and test for the boolean result of indexExists().
$index_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
$this->assertIdentical($index_exists, TRUE, 'Index created.');
// Rename the table.
db_rename_table('test_table', 'test_table2');
// Index should be renamed.
$index_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field');
$this->assertTrue($index_exists, 'Index was renamed.');
// We need the default so that we can insert after the rename.
db_field_set_default('test_table2', 'test_field', 0);
$this->assertFalse($this->tryInsert(), 'Insert into the old table failed.');
$this->assertTrue($this->tryInsert('test_table2'), 'Insert into the new table succeeded.');
// We should have successfully inserted exactly two rows.
$count = db_query('SELECT COUNT(*) FROM {test_table2}')->fetchField();
$this->assertEqual($count, 2, 'Two fields were successfully inserted.');
// Try to drop the table.
db_drop_table('test_table2');
$this->assertFalse(db_table_exists('test_table2'), 'The dropped table does not exist.');
// Recreate the table.
db_create_table('test_table', $table_specification);
db_field_set_default('test_table', 'test_field', 0);
db_add_field('test_table', 'test_serial', array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'description' => 'Added column description.'));
// Assert that the column comment has been set.
$this->checkSchemaComment('Added column description.', 'test_table', 'test_serial');
// Change the new field to a serial column.
db_change_field('test_table', 'test_serial', 'test_serial', array('type' => 'serial', 'not null' => TRUE, 'description' => 'Changed column description.'), array('primary key' => array('test_serial')));
// Assert that the column comment has been set.
$this->checkSchemaComment('Changed column description.', 'test_table', 'test_serial');
$this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.');
$max1 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField();
$this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.');
$max2 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField();
$this->assertTrue($max2 > $max1, 'The serial is monotone.');
$count = db_query('SELECT COUNT(*) FROM {test_table}')->fetchField();
$this->assertEqual($count, 2, 'There were two rows.');
// Test renaming of keys and constraints.
db_drop_table('test_table');
$table_specification = array(
'fields' => array(
'id' => array(
'type' => 'serial',
'not null' => TRUE,
),
'test_field' => array(
'type' => 'int',
'default' => 0,
),
),
'primary key' => array('id'),
'unique keys' => array(
'test_field' => array('test_field'),
),
);
db_create_table('test_table', $table_specification);
// Tests for indexes are Database specific.
$db_type = Database::getConnection()->databaseType();
// Test for existing primary and unique keys.
switch ($db_type) {
case 'pgsql':
$primary_key_exists = Database::getConnection()->schema()->constraintExists('test_table', '__pkey');
$unique_key_exists = Database::getConnection()->schema()->constraintExists('test_table', 'test_field' . '__key');
break;
case 'sqlite':
// SQLite does not create a standalone index for primary keys.
$primary_key_exists = TRUE;
$unique_key_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
break;
default:
$primary_key_exists = Database::getConnection()->schema()->indexExists('test_table', 'PRIMARY');
$unique_key_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
break;
}
$this->assertIdentical($primary_key_exists, TRUE, 'Primary key created.');
$this->assertIdentical($unique_key_exists, TRUE, 'Unique key created.');
db_rename_table('test_table', 'test_table2');
// Test for renamed primary and unique keys.
switch ($db_type) {
case 'pgsql':
$renamed_primary_key_exists = Database::getConnection()->schema()->constraintExists('test_table2', '__pkey');
$renamed_unique_key_exists = Database::getConnection()->schema()->constraintExists('test_table2', 'test_field' . '__key');
break;
case 'sqlite':
// SQLite does not create a standalone index for primary keys.
$renamed_primary_key_exists = TRUE;
$renamed_unique_key_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field');
break;
default:
$renamed_primary_key_exists = Database::getConnection()->schema()->indexExists('test_table2', 'PRIMARY');
$renamed_unique_key_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field');
break;
}
$this->assertIdentical($renamed_primary_key_exists, TRUE, 'Primary key was renamed.');
$this->assertIdentical($renamed_unique_key_exists, TRUE, 'Unique key was renamed.');
// For PostgreSQL check in addition that sequence was renamed.
if ($db_type == 'pgsql') {
// Get information about new table.
$info = Database::getConnection()->schema()->queryTableInformation('test_table2');
$sequence_name = Database::getConnection()->schema()->prefixNonTable('test_table2', 'id', 'seq');
$this->assertEqual($sequence_name, current($info->sequences), 'Sequence was renamed.');
}
// Use database specific data type and ensure that table is created.
$table_specification = array(
'description' => 'Schema table description.',
'fields' => array(
'timestamp' => array(
'mysql_type' => 'timestamp',
'pgsql_type' => 'timestamp',
'sqlite_type' => 'datetime',
'not null' => FALSE,
'default' => NULL,
),
),
);
try {
db_create_table('test_timestamp', $table_specification);
}
catch (\Exception $e) {}
$this->assertTrue(db_table_exists('test_timestamp'), 'Table with database specific datatype was created.');
}
/**
* Tests that indexes on string fields are limited to 191 characters on MySQL.
*
* @see \Drupal\Core\Database\Driver\mysql\Schema::getNormalizedIndexes()
*/
function testIndexLength() {
if (Database::getConnection()->databaseType() != 'mysql') {
return;
}
$table_specification = array(
'fields' => array(
'id' => array(
'type' => 'int',
'default' => NULL,
),
'test_field_text' => array(
'type' => 'text',
'not null' => TRUE,
),
'test_field_string_long' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
),
'test_field_string_ascii_long' => array(
'type' => 'varchar_ascii',
'length' => 255,
),
'test_field_string_short' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
),
),
'indexes' => array(
'test_regular' => array(
'test_field_text',
'test_field_string_long',
'test_field_string_ascii_long',
'test_field_string_short',
),
'test_length' => array(
array('test_field_text', 128),
array('test_field_string_long', 128),
array('test_field_string_ascii_long', 128),
array('test_field_string_short', 128),
),
'test_mixed' => array(
array('test_field_text', 200),
'test_field_string_long',
array('test_field_string_ascii_long', 200),
'test_field_string_short',
),
),
);
db_create_table('test_table_index_length', $table_specification);
// Get index information.
$results = db_query('SHOW INDEX FROM {test_table_index_length}');
$expected_lengths = array(
'test_regular' => array(
'test_field_text' => 191,
'test_field_string_long' => 191,
'test_field_string_ascii_long' => NULL,
'test_field_string_short' => NULL,
),
'test_length' => array(
'test_field_text' => 128,
'test_field_string_long' => 128,
'test_field_string_ascii_long' => 128,
'test_field_string_short' => NULL,
),
'test_mixed' => array(
'test_field_text' => 191,
'test_field_string_long' => 191,
'test_field_string_ascii_long' => 200,
'test_field_string_short' => NULL,
),
);
// Count the number of columns defined in the indexes.
$column_count = 0;
foreach ($table_specification['indexes'] as $index) {
foreach ($index as $field) {
$column_count++;
}
}
$test_count = 0;
foreach ($results as $result) {
$this->assertEqual($result->Sub_part, $expected_lengths[$result->Key_name][$result->Column_name], 'Index length matches expected value.');
$test_count++;
}
$this->assertEqual($test_count, $column_count, 'Number of tests matches expected value.');
}
/**
* Tests inserting data into an existing table.
*
* @param $table
* The database table to insert data into.
*
* @return
* TRUE if the insert succeeded, FALSE otherwise.
*/
function tryInsert($table = 'test_table') {
try {
db_insert($table)
->fields(array('id' => mt_rand(10, 20)))
->execute();
return TRUE;
}
catch (\Exception $e) {
return FALSE;
}
}
/**
* Checks that a table or column comment matches a given description.
*
* @param $description
* The asserted description.
* @param $table
* The table to test.
* @param $column
* Optional column to test.
*/
function checkSchemaComment($description, $table, $column = NULL) {
if (method_exists(Database::getConnection()->schema(), 'getComment')) {
$comment = Database::getConnection()->schema()->getComment($table, $column);
// The schema comment truncation for mysql is different.
if (Database::getConnection()->databaseType() == 'mysql') {
$max_length = $column ? 255 : 60;
$description = Unicode::truncate($description, $max_length, TRUE, TRUE);
}
$this->assertEqual($comment, $description, 'The comment matches the schema description.');
}
}
/**
* Tests creating unsigned columns and data integrity thereof.
*/
function testUnsignedColumns() {
// First create the table with just a serial column.
$table_name = 'unsigned_table';
$table_spec = array(
'fields' => array('serial_column' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE)),
'primary key' => array('serial_column'),
);
db_create_table($table_name, $table_spec);
// Now set up columns for the other types.
$types = array('int', 'float', 'numeric');
foreach ($types as $type) {
$column_spec = array('type' => $type, 'unsigned'=> TRUE);
if ($type == 'numeric') {
$column_spec += array('precision' => 10, 'scale' => 0);
}
$column_name = $type . '_column';
$table_spec['fields'][$column_name] = $column_spec;
db_add_field($table_name, $column_name, $column_spec);
}
// Finally, check each column and try to insert invalid values into them.
foreach ($table_spec['fields'] as $column_name => $column_spec) {
$this->assertTrue(db_field_exists($table_name, $column_name), format_string('Unsigned @type column was created.', array('@type' => $column_spec['type'])));
$this->assertFalse($this->tryUnsignedInsert($table_name, $column_name), format_string('Unsigned @type column rejected a negative value.', array('@type' => $column_spec['type'])));
}
}
/**
* Tries to insert a negative value into columns defined as unsigned.
*
* @param $table_name
* The table to insert.
* @param $column_name
* The column to insert.
*
* @return
* TRUE if the insert succeeded, FALSE otherwise.
*/
function tryUnsignedInsert($table_name, $column_name) {
try {
db_insert($table_name)
->fields(array($column_name => -1))
->execute();
return TRUE;
}
catch (\Exception $e) {
return FALSE;
}
}
/**
* Tests adding columns to an existing table.
*/
function testSchemaAddField() {
// Test varchar types.
foreach (array(1, 32, 128, 256, 512) as $length) {
$base_field_spec = array(
'type' => 'varchar',
'length' => $length,
);
$variations = array(
array('not null' => FALSE),
array('not null' => FALSE, 'default' => '7'),
array('not null' => FALSE, 'default' => substr('"thing"', 0, $length)),
array('not null' => FALSE, 'default' => substr("\"'hing", 0, $length)),
array('not null' => TRUE, 'initial' => 'd'),
array('not null' => FALSE, 'default' => NULL),
array('not null' => TRUE, 'initial' => 'd', 'default' => '7'),
);
foreach ($variations as $variation) {
$field_spec = $variation + $base_field_spec;
$this->assertFieldAdditionRemoval($field_spec);
}
}
// Test int and float types.
foreach (array('int', 'float') as $type) {
foreach (array('tiny', 'small', 'medium', 'normal', 'big') as $size) {
$base_field_spec = array(
'type' => $type,
'size' => $size,
);
$variations = array(
array('not null' => FALSE),
array('not null' => FALSE, 'default' => 7),
array('not null' => TRUE, 'initial' => 1),
array('not null' => TRUE, 'initial' => 1, 'default' => 7),
);
foreach ($variations as $variation) {
$field_spec = $variation + $base_field_spec;
$this->assertFieldAdditionRemoval($field_spec);
}
}
}
// Test numeric types.
foreach (array(1, 5, 10, 40, 65) as $precision) {
foreach (array(0, 2, 10, 30) as $scale) {
// Skip combinations where precision is smaller than scale.
if ($precision <= $scale) {
continue;
}
$base_field_spec = array(
'type' => 'numeric',
'scale' => $scale,
'precision' => $precision,
);
$variations = array(
array('not null' => FALSE),
array('not null' => FALSE, 'default' => 7),
array('not null' => TRUE, 'initial' => 1),
array('not null' => TRUE, 'initial' => 1, 'default' => 7),
);
foreach ($variations as $variation) {
$field_spec = $variation + $base_field_spec;
$this->assertFieldAdditionRemoval($field_spec);
}
}
}
}
/**
* Asserts that a given field can be added and removed from a table.
*
* The addition test covers both defining a field of a given specification
* when initially creating at table and extending an existing table.
*
* @param $field_spec
* The schema specification of the field.
*/
protected function assertFieldAdditionRemoval($field_spec) {
// Try creating the field on a new table.
$table_name = 'test_table_' . ($this->counter++);
$table_spec = array(
'fields' => array(
'serial_column' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
'test_field' => $field_spec,
),
'primary key' => array('serial_column'),
);
db_create_table($table_name, $table_spec);
$this->pass(format_string('Table %table created.', array('%table' => $table_name)));
// Check the characteristics of the field.
$this->assertFieldCharacteristics($table_name, 'test_field', $field_spec);
// Clean-up.
db_drop_table($table_name);
// Try adding a field to an existing table.
$table_name = 'test_table_' . ($this->counter++);
$table_spec = array(
'fields' => array(
'serial_column' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
),
'primary key' => array('serial_column'),
);
db_create_table($table_name, $table_spec);
$this->pass(format_string('Table %table created.', array('%table' => $table_name)));
// Insert some rows to the table to test the handling of initial values.
for ($i = 0; $i < 3; $i++) {
db_insert($table_name)
->useDefaults(array('serial_column'))
->execute();
}
db_add_field($table_name, 'test_field', $field_spec);
$this->pass(format_string('Column %column created.', array('%column' => 'test_field')));
// Check the characteristics of the field.
$this->assertFieldCharacteristics($table_name, 'test_field', $field_spec);
// Clean-up.
db_drop_field($table_name, 'test_field');
// Add back the field and then try to delete a field which is also a primary
// key.
db_add_field($table_name, 'test_field', $field_spec);
db_drop_field($table_name, 'serial_column');
db_drop_table($table_name);
}
/**
* Asserts that a newly added field has the correct characteristics.
*/
protected function assertFieldCharacteristics($table_name, $field_name, $field_spec) {
// Check that the initial value has been registered.
if (isset($field_spec['initial'])) {
// There should be no row with a value different then $field_spec['initial'].
$count = db_select($table_name)
->fields($table_name, array('serial_column'))
->condition($field_name, $field_spec['initial'], '<>')
->countQuery()
->execute()
->fetchField();
$this->assertEqual($count, 0, 'Initial values filled out.');
}
// Check that the default value has been registered.
if (isset($field_spec['default'])) {
// Try inserting a row, and check the resulting value of the new column.
$id = db_insert($table_name)
->useDefaults(array('serial_column'))
->execute();
$field_value = db_select($table_name)
->fields($table_name, array($field_name))
->condition('serial_column', $id)
->execute()
->fetchField();
$this->assertEqual($field_value, $field_spec['default'], 'Default value registered.');
}
}
/**
* Tests changing columns between numeric types.
*/
function testSchemaChangeField() {
$field_specs = array(
array('type' => 'int', 'size' => 'normal','not null' => FALSE),
array('type' => 'int', 'size' => 'normal', 'not null' => TRUE, 'initial' => 1, 'default' => 17),
array('type' => 'float', 'size' => 'normal', 'not null' => FALSE),
array('type' => 'float', 'size' => 'normal', 'not null' => TRUE, 'initial' => 1, 'default' => 7.3),
array('type' => 'numeric', 'scale' => 2, 'precision' => 10, 'not null' => FALSE),
array('type' => 'numeric', 'scale' => 2, 'precision' => 10, 'not null' => TRUE, 'initial' => 1, 'default' => 7),
);
foreach ($field_specs as $i => $old_spec) {
foreach ($field_specs as $j => $new_spec) {
if ($i === $j) {
// Do not change a field into itself.
continue;
}
$this->assertFieldChange($old_spec, $new_spec);
}
}
}
/**
* Asserts that a field can be changed from one spec to another.
*
* @param $old_spec
* The beginning field specification.
* @param $new_spec
* The ending field specification.
*/
protected function assertFieldChange($old_spec, $new_spec) {
$table_name = 'test_table_' . ($this->counter++);
$table_spec = array(
'fields' => array(
'serial_column' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
'test_field' => $old_spec,
),
'primary key' => array('serial_column'),
);
db_create_table($table_name, $table_spec);
$this->pass(format_string('Table %table created.', array('%table' => $table_name)));
// Check the characteristics of the field.
$this->assertFieldCharacteristics($table_name, 'test_field', $old_spec);
// Remove inserted rows.
db_truncate($table_name)->execute();
// Change the field.
db_change_field($table_name, 'test_field', 'test_field', $new_spec);
// Check the field was changed.
$this->assertFieldCharacteristics($table_name, 'test_field', $new_spec);
// Clean-up.
db_drop_table($table_name);
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectCloneTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests cloning Select queries.
*
* @group Database
*/
class SelectCloneTest extends DatabaseTestBase {
/**
* Test that subqueries as value within conditions are cloned properly.
*/
function testSelectConditionSubQueryCloning() {
$subquery = db_select('test', 't');
$subquery->addField('t', 'id', 'id');
$subquery->condition('age', 28, '<');
$query = db_select('test', 't');
$query->addField('t', 'name', 'name');
$query->condition('id', $subquery, 'IN');
$clone = clone $query;
// Cloned query should not be altered by the following modification
// happening on original query.
$subquery->condition('age', 25, '>');
$clone_result = $clone->countQuery()->execute()->fetchField();
$query_result = $query->countQuery()->execute()->fetchField();
// Make sure the cloned query has not been modified
$this->assertEqual(3, $clone_result, 'The cloned query returns the expected number of rows');
$this->assertEqual(2, $query_result, 'The query returns the expected number of rows');
}
}

View file

@ -0,0 +1,382 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectComplexTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\RowCountException;
/**
* Tests the Select query builder with more complex queries.
*
* @group Database
*/
class SelectComplexTest extends DatabaseTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'user', 'node_access_test', 'field');
/**
* Tests simple JOIN statements.
*/
function testDefaultJoin() {
$query = db_select('test_task', 't');
$people_alias = $query->join('test', 'p', 't.pid = p.id');
$name_field = $query->addField($people_alias, 'name', 'name');
$query->addField('t', 'task', 'task');
$priority_field = $query->addField('t', 'priority', 'priority');
$query->orderBy($priority_field);
$result = $query->execute();
$num_records = 0;
$last_priority = 0;
foreach ($result as $record) {
$num_records++;
$this->assertTrue($record->$priority_field >= $last_priority, 'Results returned in correct order.');
$this->assertNotEqual($record->$name_field, 'Ringo', 'Taskless person not selected.');
$last_priority = $record->$priority_field;
}
$this->assertEqual($num_records, 7, 'Returned the correct number of rows.');
}
/**
* Tests LEFT OUTER joins.
*/
function testLeftOuterJoin() {
$query = db_select('test', 'p');
$people_alias = $query->leftJoin('test_task', 't', 't.pid = p.id');
$name_field = $query->addField('p', 'name', 'name');
$query->addField($people_alias, 'task', 'task');
$query->addField($people_alias, 'priority', 'priority');
$query->orderBy($name_field);
$result = $query->execute();
$num_records = 0;
$last_name = 0;
foreach ($result as $record) {
$num_records++;
$this->assertTrue(strcmp($record->$name_field, $last_name) >= 0, 'Results returned in correct order.');
}
$this->assertEqual($num_records, 8, 'Returned the correct number of rows.');
}
/**
* Tests GROUP BY clauses.
*/
function testGroupBy() {
$query = db_select('test_task', 't');
$count_field = $query->addExpression('COUNT(task)', 'num');
$task_field = $query->addField('t', 'task');
$query->orderBy($count_field);
$query->groupBy($task_field);
$result = $query->execute();
$num_records = 0;
$last_count = 0;
$records = array();
foreach ($result as $record) {
$num_records++;
$this->assertTrue($record->$count_field >= $last_count, 'Results returned in correct order.');
$last_count = $record->$count_field;
$records[$record->$task_field] = $record->$count_field;
}
$correct_results = array(
'eat' => 1,
'sleep' => 2,
'code' => 1,
'found new band' => 1,
'perform at superbowl' => 1,
);
foreach ($correct_results as $task => $count) {
$this->assertEqual($records[$task], $count, format_string("Correct number of '@task' records found.", array('@task' => $task)));
}
$this->assertEqual($num_records, 6, 'Returned the correct number of total rows.');
}
/**
* Tests GROUP BY and HAVING clauses together.
*/
function testGroupByAndHaving() {
$query = db_select('test_task', 't');
$count_field = $query->addExpression('COUNT(task)', 'num');
$task_field = $query->addField('t', 'task');
$query->orderBy($count_field);
$query->groupBy($task_field);
$query->having('COUNT(task) >= 2');
$result = $query->execute();
$num_records = 0;
$last_count = 0;
$records = array();
foreach ($result as $record) {
$num_records++;
$this->assertTrue($record->$count_field >= 2, 'Record has the minimum count.');
$this->assertTrue($record->$count_field >= $last_count, 'Results returned in correct order.');
$last_count = $record->$count_field;
$records[$record->$task_field] = $record->$count_field;
}
$correct_results = array(
'sleep' => 2,
);
foreach ($correct_results as $task => $count) {
$this->assertEqual($records[$task], $count, format_string("Correct number of '@task' records found.", array('@task' => $task)));
}
$this->assertEqual($num_records, 1, 'Returned the correct number of total rows.');
}
/**
* Tests range queries.
*
* The SQL clause varies with the database.
*/
function testRange() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->range(0, 2);
$query_result = $query->countQuery()->execute()->fetchField();
$this->assertEqual($query_result, 2, 'Returned the correct number of rows.');
}
/**
* Tests distinct queries.
*/
function testDistinct() {
$query = db_select('test_task');
$query->addField('test_task', 'task');
$query->distinct();
$query_result = $query->countQuery()->execute()->fetchField();
$this->assertEqual($query_result, 6, 'Returned the correct number of rows.');
}
/**
* Tests that we can generate a count query from a built query.
*/
function testCountQuery() {
$query = db_select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$query->orderBy('name');
$count = $query->countQuery()->execute()->fetchField();
$this->assertEqual($count, 4, 'Counted the correct number of records.');
// Now make sure we didn't break the original query! We should still have
// all of the fields we asked for.
$record = $query->execute()->fetch();
$this->assertEqual($record->$name_field, 'George', 'Correct data retrieved.');
$this->assertEqual($record->$age_field, 27, 'Correct data retrieved.');
}
/**
* Tests having queries.
*/
function testHavingCountQuery() {
$query = db_select('test')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->groupBy('age')
->having('age + 1 > 0');
$query->addField('test', 'age');
$query->addExpression('age + 1');
$count = count($query->execute()->fetchCol());
$this->assertEqual($count, 4, 'Counted the correct number of records.');
}
/**
* Tests that countQuery removes 'all_fields' statements and ordering clauses.
*/
function testCountQueryRemovals() {
$query = db_select('test');
$query->fields('test');
$query->orderBy('name');
$count = $query->countQuery();
// Check that the 'all_fields' statement is handled properly.
$tables = $query->getTables();
$this->assertEqual($tables['test']['all_fields'], 1, 'Query correctly sets \'all_fields\' statement.');
$tables = $count->getTables();
$this->assertFalse(isset($tables['test']['all_fields']), 'Count query correctly unsets \'all_fields\' statement.');
// Check that the ordering clause is handled properly.
$orderby = $query->getOrderBy();
// The orderby string is different for PostgreSQL.
// @see Drupal\Core\Database\Driver\pgsql\Select::orderBy()
$db_type = Database::getConnection()->databaseType();
$this->assertEqual($orderby['name'], ($db_type == 'pgsql' ? 'ASC NULLS FIRST' : 'ASC'), 'Query correctly sets ordering clause.');
$orderby = $count->getOrderBy();
$this->assertFalse(isset($orderby['name']), 'Count query correctly unsets ordering clause.');
// Make sure that the count query works.
$count = $count->execute()->fetchField();
$this->assertEqual($count, 4, 'Counted the correct number of records.');
}
/**
* Tests that countQuery properly removes fields and expressions.
*/
function testCountQueryFieldRemovals() {
// countQuery should remove all fields and expressions, so this can be
// tested by adding a non-existent field and expression: if it ends
// up in the query, an error will be thrown. If not, it will return the
// number of records, which in this case happens to be 4 (there are four
// records in the {test} table).
$query = db_select('test');
$query->fields('test', array('fail'));
$this->assertEqual(4, $query->countQuery()->execute()->fetchField(), 'Count Query removed fields');
$query = db_select('test');
$query->addExpression('fail');
$this->assertEqual(4, $query->countQuery()->execute()->fetchField(), 'Count Query removed expressions');
}
/**
* Tests that we can generate a count query from a query with distinct.
*/
function testCountQueryDistinct() {
$query = db_select('test_task');
$query->addField('test_task', 'task');
$query->distinct();
$count = $query->countQuery()->execute()->fetchField();
$this->assertEqual($count, 6, 'Counted the correct number of records.');
}
/**
* Tests that we can generate a count query from a query with GROUP BY.
*/
function testCountQueryGroupBy() {
$query = db_select('test_task');
$query->addField('test_task', 'pid');
$query->groupBy('pid');
$count = $query->countQuery()->execute()->fetchField();
$this->assertEqual($count, 3, 'Counted the correct number of records.');
// Use a column alias as, without one, the query can succeed for the wrong
// reason.
$query = db_select('test_task');
$query->addField('test_task', 'pid', 'pid_alias');
$query->addExpression('COUNT(test_task.task)', 'count');
$query->groupBy('pid_alias');
$query->orderBy('pid_alias', 'asc');
$count = $query->countQuery()->execute()->fetchField();
$this->assertEqual($count, 3, 'Counted the correct number of records.');
}
/**
* Confirms that we can properly nest conditional clauses.
*/
function testNestedConditions() {
// This query should translate to:
// "SELECT job FROM {test} WHERE name = 'Paul' AND (age = 26 OR age = 27)"
// That should find only one record. Yes it's a non-optimal way of writing
// that query but that's not the point!
$query = db_select('test');
$query->addField('test', 'job');
$query->condition('name', 'Paul');
$query->condition(db_or()->condition('age', 26)->condition('age', 27));
$job = $query->execute()->fetchField();
$this->assertEqual($job, 'Songwriter', 'Correct data retrieved.');
}
/**
* Confirms we can join on a single table twice with a dynamic alias.
*/
function testJoinTwice() {
$query = db_select('test')->fields('test');
$alias = $query->join('test', 'test', 'test.job = %alias.job');
$query->addField($alias, 'name', 'othername');
$query->addField($alias, 'job', 'otherjob');
$query->where("$alias.name <> test.name");
$crowded_job = $query->execute()->fetch();
$this->assertEqual($crowded_job->job, $crowded_job->otherjob, 'Correctly joined same table twice.');
$this->assertNotEqual($crowded_job->name, $crowded_job->othername, 'Correctly joined same table twice.');
}
/**
* Tests that we can join on a query.
*/
function testJoinSubquery() {
$this->installSchema('system', 'sequences');
$account = entity_create('user', array(
'name' => $this->randomMachineName(),
'mail' => $this->randomMachineName() . '@example.com',
));
$query = db_select('test_task', 'tt', array('target' => 'replica'));
$query->addExpression('tt.pid + 1', 'abc');
$query->condition('priority', 1, '>');
$query->condition('priority', 100, '<');
$subquery = db_select('test', 'tp');
$subquery->join('test_one_blob', 'tpb', 'tp.id = tpb.id');
$subquery->join('node', 'n', 'tp.id = n.nid');
$subquery->addTag('node_access');
$subquery->addMetaData('account', $account);
$subquery->addField('tp', 'id');
$subquery->condition('age', 5, '>');
$subquery->condition('age', 500, '<');
$query->leftJoin($subquery, 'sq', 'tt.pid = sq.id');
$query->join('test_one_blob', 'tb3', 'tt.pid = tb3.id');
// Construct the query string.
// This is the same sequence that SelectQuery::execute() goes through.
$query->preExecute();
$query->getArguments();
$str = (string) $query;
// Verify that the string only has one copy of condition placeholder 0.
$pos = strpos($str, 'db_condition_placeholder_0', 0);
$pos2 = strpos($str, 'db_condition_placeholder_0', $pos + 1);
$this->assertFalse($pos2, 'Condition placeholder is not repeated.');
}
/**
* Tests that rowCount() throws exception on SELECT query.
*/
function testSelectWithRowCount() {
$query = db_select('test');
$query->addField('test', 'name');
$result = $query->execute();
try {
$result->rowCount();
$exception = FALSE;
}
catch (RowCountException $e) {
$exception = TRUE;
}
$this->assertTrue($exception, 'Exception was thrown');
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectOrderedTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the Select query builder.
*
* @group Database
*/
class SelectOrderedTest extends DatabaseTestBase {
/**
* Tests basic ORDER BY.
*/
function testSimpleSelectOrdered() {
$query = db_select('test');
$query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$query->orderBy($age_field);
$result = $query->execute();
$num_records = 0;
$last_age = 0;
foreach ($result as $record) {
$num_records++;
$this->assertTrue($record->age >= $last_age, 'Results returned in correct order.');
$last_age = $record->age;
}
$this->assertEqual($num_records, 4, 'Returned the correct number of rows.');
}
/**
* Tests multiple ORDER BY.
*/
function testSimpleSelectMultiOrdered() {
$query = db_select('test');
$query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$job_field = $query->addField('test', 'job');
$query->orderBy($job_field);
$query->orderBy($age_field);
$result = $query->execute();
$num_records = 0;
$expected = array(
array('Ringo', 28, 'Drummer'),
array('John', 25, 'Singer'),
array('George', 27, 'Singer'),
array('Paul', 26, 'Songwriter'),
);
$results = $result->fetchAll(\PDO::FETCH_NUM);
foreach ($expected as $k => $record) {
$num_records++;
foreach ($record as $kk => $col) {
if ($expected[$k][$kk] != $results[$k][$kk]) {
$this->assertTrue(FALSE, 'Results returned in correct order.');
}
}
}
$this->assertEqual($num_records, 4, 'Returned the correct number of rows.');
}
/**
* Tests ORDER BY descending.
*/
function testSimpleSelectOrderedDesc() {
$query = db_select('test');
$query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$query->orderBy($age_field, 'DESC');
$result = $query->execute();
$num_records = 0;
$last_age = 100000000;
foreach ($result as $record) {
$num_records++;
$this->assertTrue($record->age <= $last_age, 'Results returned in correct order.');
$last_age = $record->age;
}
$this->assertEqual($num_records, 4, 'Returned the correct number of rows.');
}
}

View file

@ -0,0 +1,172 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectPagerDefaultTest.
*/
namespace Drupal\system\Tests\Database;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the pager query select extender.
*
* @group Database
*/
class SelectPagerDefaultTest extends DatabaseWebTestBase {
/**
* Confirms that a pager query returns the correct results.
*
* Note that we have to make an HTTP request to a test page handler
* because the pager depends on GET parameters.
*/
function testEvenPagerQuery() {
// To keep the test from being too brittle, we determine up front
// what the page count should be dynamically, and pass the control
// information forward to the actual query on the other side of the
// HTTP request.
$limit = 2;
$count = db_query('SELECT COUNT(*) FROM {test}')->fetchField();
$correct_number = $limit;
$num_pages = floor($count / $limit);
// If there is no remainder from rounding, subtract 1 since we index from 0.
if (!($num_pages * $limit < $count)) {
$num_pages--;
}
for ($page = 0; $page <= $num_pages; ++$page) {
$this->drupalGet('database_test/pager_query_even/' . $limit, array('query' => array('page' => $page)));
$data = json_decode($this->getRawContent());
if ($page == $num_pages) {
$correct_number = $count - ($limit * $page);
}
$this->assertEqual(count($data->names), $correct_number, format_string('Correct number of records returned by pager: @number', array('@number' => $correct_number)));
}
}
/**
* Confirms that a pager query returns the correct results.
*
* Note that we have to make an HTTP request to a test page handler
* because the pager depends on GET parameters.
*/
function testOddPagerQuery() {
// To keep the test from being too brittle, we determine up front
// what the page count should be dynamically, and pass the control
// information forward to the actual query on the other side of the
// HTTP request.
$limit = 2;
$count = db_query('SELECT COUNT(*) FROM {test_task}')->fetchField();
$correct_number = $limit;
$num_pages = floor($count / $limit);
// If there is no remainder from rounding, subtract 1 since we index from 0.
if (!($num_pages * $limit < $count)) {
$num_pages--;
}
for ($page = 0; $page <= $num_pages; ++$page) {
$this->drupalGet('database_test/pager_query_odd/' . $limit, array('query' => array('page' => $page)));
$data = json_decode($this->getRawContent());
if ($page == $num_pages) {
$correct_number = $count - ($limit * $page);
}
$this->assertEqual(count($data->names), $correct_number, format_string('Correct number of records returned by pager: @number', array('@number' => $correct_number)));
}
}
/**
* Confirms that a pager query results with an inner pager query are valid.
*
* This is a regression test for #467984.
*/
function testInnerPagerQuery() {
$query = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender');
$query
->fields('t', array('age'))
->orderBy('age')
->limit(5);
$outer_query = db_select($query);
$outer_query->addField('subquery', 'age');
$ages = $outer_query
->execute()
->fetchCol();
$this->assertEqual($ages, array(25, 26, 27, 28), 'Inner pager query returned the correct ages.');
}
/**
* Confirms that a paging query results with a having expression are valid.
*
* This is a regression test for #467984.
*/
function testHavingPagerQuery() {
$query = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender');
$query
->fields('t', array('name'))
->orderBy('name')
->groupBy('name')
->having('MAX(age) > :count', array(':count' => 26))
->limit(5);
$ages = $query
->execute()
->fetchCol();
$this->assertEqual($ages, array('George', 'Ringo'), 'Pager query with having expression returned the correct ages.');
}
/**
* Confirms that every pager gets a valid, non-overlapping element ID.
*/
function testElementNumbers() {
$request = Request::createFromGlobals();
$request->query->replace(array(
'page' => '3, 2, 1, 0',
));
\Drupal::getContainer()->get('request_stack')->push($request);
$name = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->element(2)
->fields('t', array('name'))
->orderBy('age')
->limit(1)
->execute()
->fetchField();
$this->assertEqual($name, 'Paul', 'Pager query #1 with a specified element ID returned the correct results.');
// Setting an element smaller than the previous one
// should not overwrite the pager $maxElement with a smaller value.
$name = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->element(1)
->fields('t', array('name'))
->orderBy('age')
->limit(1)
->execute()
->fetchField();
$this->assertEqual($name, 'George', 'Pager query #2 with a specified element ID returned the correct results.');
$name = db_select('test', 't')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->fields('t', array('name'))
->orderBy('age')
->limit(1)
->execute()
->fetchField();
$this->assertEqual($name, 'John', 'Pager query #3 with a generated element ID returned the correct results.');
}
}

View file

@ -0,0 +1,183 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectSubqueryTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the Select query builder.
*
* @group Database
*/
class SelectSubqueryTest extends DatabaseTestBase {
/**
* Tests that we can use a subquery in a FROM clause.
*/
function testFromSubquerySelect() {
// Create a subquery, which is just a normal query object.
$subquery = db_select('test_task', 'tt');
$subquery->addField('tt', 'pid', 'pid');
$subquery->addField('tt', 'task', 'task');
$subquery->condition('priority', 1);
for ($i = 0; $i < 2; $i++) {
// Create another query that joins against the virtual table resulting
// from the subquery.
$select = db_select($subquery, 'tt2');
$select->join('test', 't', 't.id=tt2.pid');
$select->addField('t', 'name');
if ($i) {
// Use a different number of conditions here to confuse the subquery
// placeholder counter, testing https://www.drupal.org/node/1112854.
$select->condition('name', 'John');
}
$select->condition('task', 'code');
// The resulting query should be equivalent to:
// SELECT t.name
// FROM (SELECT tt.pid AS pid, tt.task AS task FROM test_task tt WHERE priority=1) tt
// INNER JOIN test t ON t.id=tt.pid
// WHERE tt.task = 'code'
$people = $select->execute()->fetchCol();
$this->assertEqual(count($people), 1, 'Returned the correct number of rows.');
}
}
/**
* Tests that we can use a subquery in a FROM clause with a LIMIT.
*/
function testFromSubquerySelectWithLimit() {
// Create a subquery, which is just a normal query object.
$subquery = db_select('test_task', 'tt');
$subquery->addField('tt', 'pid', 'pid');
$subquery->addField('tt', 'task', 'task');
$subquery->orderBy('priority', 'DESC');
$subquery->range(0, 1);
// Create another query that joins against the virtual table resulting
// from the subquery.
$select = db_select($subquery, 'tt2');
$select->join('test', 't', 't.id=tt2.pid');
$select->addField('t', 'name');
// The resulting query should be equivalent to:
// SELECT t.name
// FROM (SELECT tt.pid AS pid, tt.task AS task FROM test_task tt ORDER BY priority DESC LIMIT 1 OFFSET 0) tt
// INNER JOIN test t ON t.id=tt.pid
$people = $select->execute()->fetchCol();
$this->assertEqual(count($people), 1, 'Returned the correct number of rows.');
}
/**
* Tests that we can use a subquery in a WHERE clause.
*/
function testConditionSubquerySelect() {
// Create a subquery, which is just a normal query object.
$subquery = db_select('test_task', 'tt');
$subquery->addField('tt', 'pid', 'pid');
$subquery->condition('tt.priority', 1);
// Create another query that joins against the virtual table resulting
// from the subquery.
$select = db_select('test_task', 'tt2');
$select->addField('tt2', 'task');
$select->condition('tt2.pid', $subquery, 'IN');
// The resulting query should be equivalent to:
// SELECT tt2.name
// FROM test tt2
// WHERE tt2.pid IN (SELECT tt.pid AS pid FROM test_task tt WHERE tt.priority=1)
$people = $select->execute()->fetchCol();
$this->assertEqual(count($people), 5, 'Returned the correct number of rows.');
}
/**
* Tests that we can use a subquery in a JOIN clause.
*/
function testJoinSubquerySelect() {
// Create a subquery, which is just a normal query object.
$subquery = db_select('test_task', 'tt');
$subquery->addField('tt', 'pid', 'pid');
$subquery->condition('priority', 1);
// Create another query that joins against the virtual table resulting
// from the subquery.
$select = db_select('test', 't');
$select->join($subquery, 'tt', 't.id=tt.pid');
$select->addField('t', 'name');
// The resulting query should be equivalent to:
// SELECT t.name
// FROM test t
// INNER JOIN (SELECT tt.pid AS pid FROM test_task tt WHERE priority=1) tt ON t.id=tt.pid
$people = $select->execute()->fetchCol();
$this->assertEqual(count($people), 2, 'Returned the correct number of rows.');
}
/**
* Tests EXISTS subquery conditionals on SELECT statements.
*
* We essentially select all rows from the {test} table that have matching
* rows in the {test_people} table based on the shared name column.
*/
function testExistsSubquerySelect() {
// Put George into {test_people}.
db_insert('test_people')
->fields(array(
'name' => 'George',
'age' => 27,
'job' => 'Singer',
))
->execute();
// Base query to {test}.
$query = db_select('test', 't')
->fields('t', array('name'));
// Subquery to {test_people}.
$subquery = db_select('test_people', 'tp')
->fields('tp', array('name'))
->where('tp.name = t.name');
$query->exists($subquery);
$result = $query->execute();
// Ensure that we got the right record.
$record = $result->fetch();
$this->assertEqual($record->name, 'George', 'Fetched name is correct using EXISTS query.');
}
/**
* Tests NOT EXISTS subquery conditionals on SELECT statements.
*
* We essentially select all rows from the {test} table that don't have
* matching rows in the {test_people} table based on the shared name column.
*/
function testNotExistsSubquerySelect() {
// Put George into {test_people}.
db_insert('test_people')
->fields(array(
'name' => 'George',
'age' => 27,
'job' => 'Singer',
))
->execute();
// Base query to {test}.
$query = db_select('test', 't')
->fields('t', array('name'));
// Subquery to {test_people}.
$subquery = db_select('test_people', 'tp')
->fields('tp', array('name'))
->where('tp.name = t.name');
$query->notExists($subquery);
// Ensure that we got the right number of records.
$people = $query->execute()->fetchCol();
$this->assertEqual(count($people), 3, 'NOT EXISTS query returned the correct results.');
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectTableSortDefaultTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the tablesort query extender.
*
* @group Database
*/
class SelectTableSortDefaultTest extends DatabaseWebTestBase {
/**
* Confirms that a tablesort query returns the correct results.
*
* Note that we have to make an HTTP request to a test page handler
* because the pager depends on GET parameters.
*/
function testTableSortQuery() {
$sorts = array(
array('field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'),
array('field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'),
array('field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'),
array('field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'),
// more elements here
);
foreach ($sorts as $sort) {
$this->drupalGet('database_test/tablesort/', array('query' => array('order' => $sort['field'], 'sort' => $sort['sort'])));
$data = json_decode($this->getRawContent());
$first = array_shift($data->tasks);
$last = array_pop($data->tasks);
$this->assertEqual($first->task, $sort['first'], 'Items appear in the correct order.');
$this->assertEqual($last->task, $sort['last'], 'Items appear in the correct order.');
}
}
/**
* Confirms precedence of tablesorts headers.
*
* If a tablesort's orderByHeader is called before another orderBy, then its
* header happens first.
*/
function testTableSortQueryFirst() {
$sorts = array(
array('field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'),
array('field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'),
array('field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'),
array('field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'),
// more elements here
);
foreach ($sorts as $sort) {
$this->drupalGet('database_test/tablesort_first/', array('query' => array('order' => $sort['field'], 'sort' => $sort['sort'])));
$data = json_decode($this->getRawContent());
$first = array_shift($data->tasks);
$last = array_pop($data->tasks);
$this->assertEqual($first->task, $sort['first'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort'])));
$this->assertEqual($last->task, $sort['last'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort'])));
}
}
/**
* Confirms that tableselect is rendered without error.
*
* Specifically that no sort is set in a tableselect, and that header links
* are correct.
*/
function testTableSortDefaultSort() {
$this->drupalGet('database_test/tablesort_default_sort');
// Verify that the table was displayed. Just the header is checked for
// because if there were any fatal errors or exceptions in displaying the
// sorted table, it would not print the table.
$this->assertText(t('Username'));
// Verify that the header links are built properly.
$this->assertLinkByHref('database_test/tablesort_default_sort');
$this->assertPattern('/\<a.*title\=\"' . t('sort by Username') . '\".*\>/');
}
}

View file

@ -0,0 +1,497 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SelectTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\InvalidQueryException;
/**
* Tests the Select query builder.
*
* @group Database
*/
class SelectTest extends DatabaseTestBase {
/**
* Tests rudimentary SELECT statements.
*/
function testSimpleSelect() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$num_records = $query->countQuery()->execute()->fetchField();
$this->assertEqual($num_records, 4, 'Returned the correct number of rows.');
}
/**
* Tests rudimentary SELECT statement with a COMMENT.
*/
function testSimpleComment() {
$query = db_select('test')->comment('Testing query comments');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$result = $query->execute();
$records = $result->fetchAll();
$query = (string) $query;
$expected = "/* Testing query comments */";
$this->assertEqual(count($records), 4, 'Returned the correct number of rows.');
$this->assertNotIdentical(FALSE, strpos($query, $expected), 'The flattened query contains the comment string.');
}
/**
* Tests query COMMENT system against vulnerabilities.
*/
function testVulnerableComment() {
$query = db_select('test')->comment('Testing query comments */ SELECT nid FROM {node}; --');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$result = $query->execute();
$records = $result->fetchAll();
$query = (string) $query;
$expected = "/* Testing query comments SELECT nid FROM {node}; -- */";
$this->assertEqual(count($records), 4, 'Returned the correct number of rows.');
$this->assertNotIdentical(FALSE, strpos($query, $expected), 'The flattened query contains the sanitised comment string.');
}
/**
* Tests basic conditionals on SELECT statements.
*/
function testSimpleSelectConditional() {
$query = db_select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addField('test', 'age', 'age');
$query->condition('age', 27);
$result = $query->execute();
// Check that the aliases are being created the way we want.
$this->assertEqual($name_field, 'name', 'Name field alias is correct.');
$this->assertEqual($age_field, 'age', 'Age field alias is correct.');
// Ensure that we got the right record.
$record = $result->fetch();
$this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.');
$this->assertEqual($record->$age_field, 27, 'Fetched age is correct.');
}
/**
* Tests SELECT statements with expressions.
*/
function testSimpleSelectExpression() {
$query = db_select('test');
$name_field = $query->addField('test', 'name');
$age_field = $query->addExpression("age*2", 'double_age');
$query->condition('age', 27);
$result = $query->execute();
// Check that the aliases are being created the way we want.
$this->assertEqual($name_field, 'name', 'Name field alias is correct.');
$this->assertEqual($age_field, 'double_age', 'Age field alias is correct.');
// Ensure that we got the right record.
$record = $result->fetch();
$this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.');
$this->assertEqual($record->$age_field, 27*2, 'Fetched age expression is correct.');
}
/**
* Tests SELECT statements with multiple expressions.
*/
function testSimpleSelectExpressionMultiple() {
$query = db_select('test');
$name_field = $query->addField('test', 'name');
$age_double_field = $query->addExpression("age*2");
$age_triple_field = $query->addExpression("age*3");
$query->condition('age', 27);
$result = $query->execute();
// Check that the aliases are being created the way we want.
$this->assertEqual($age_double_field, 'expression', 'Double age field alias is correct.');
$this->assertEqual($age_triple_field, 'expression_2', 'Triple age field alias is correct.');
// Ensure that we got the right record.
$record = $result->fetch();
$this->assertEqual($record->$name_field, 'George', 'Fetched name is correct.');
$this->assertEqual($record->$age_double_field, 27*2, 'Fetched double age expression is correct.');
$this->assertEqual($record->$age_triple_field, 27*3, 'Fetched triple age expression is correct.');
}
/**
* Tests adding multiple fields to a SELECT statement at the same time.
*/
function testSimpleSelectMultipleFields() {
$record = db_select('test')
->fields('test', array('id', 'name', 'age', 'job'))
->condition('age', 27)
->execute()->fetchObject();
// Check that all fields we asked for are present.
$this->assertNotNull($record->id, 'ID field is present.');
$this->assertNotNull($record->name, 'Name field is present.');
$this->assertNotNull($record->age, 'Age field is present.');
$this->assertNotNull($record->job, 'Job field is present.');
// Ensure that we got the right record.
// Check that all fields we asked for are present.
$this->assertEqual($record->id, 2, 'ID field has the correct value.');
$this->assertEqual($record->name, 'George', 'Name field has the correct value.');
$this->assertEqual($record->age, 27, 'Age field has the correct value.');
$this->assertEqual($record->job, 'Singer', 'Job field has the correct value.');
}
/**
* Tests adding all fields from a given table to a SELECT statement.
*/
function testSimpleSelectAllFields() {
$record = db_select('test')
->fields('test')
->condition('age', 27)
->execute()->fetchObject();
// Check that all fields we asked for are present.
$this->assertNotNull($record->id, 'ID field is present.');
$this->assertNotNull($record->name, 'Name field is present.');
$this->assertNotNull($record->age, 'Age field is present.');
$this->assertNotNull($record->job, 'Job field is present.');
// Ensure that we got the right record.
// Check that all fields we asked for are present.
$this->assertEqual($record->id, 2, 'ID field has the correct value.');
$this->assertEqual($record->name, 'George', 'Name field has the correct value.');
$this->assertEqual($record->age, 27, 'Age field has the correct value.');
$this->assertEqual($record->job, 'Singer', 'Job field has the correct value.');
}
/**
* Tests that a comparison with NULL is always FALSE.
*/
function testNullCondition() {
$this->ensureSampleDataNull();
$names = db_select('test_null', 'tn')
->fields('tn', array('name'))
->condition('age', NULL)
->execute()->fetchCol();
$this->assertEqual(count($names), 0, 'No records found when comparing to NULL.');
}
/**
* Tests that we can find a record with a NULL value.
*/
function testIsNullCondition() {
$this->ensureSampleDataNull();
$names = db_select('test_null', 'tn')
->fields('tn', array('name'))
->isNull('age')
->execute()->fetchCol();
$this->assertEqual(count($names), 1, 'Correct number of records found with NULL age.');
$this->assertEqual($names[0], 'Fozzie', 'Correct record returned for NULL age.');
}
/**
* Tests that we can find a record without a NULL value.
*/
function testIsNotNullCondition() {
$this->ensureSampleDataNull();
$names = db_select('test_null', 'tn')
->fields('tn', array('name'))
->isNotNull('tn.age')
->orderBy('name')
->execute()->fetchCol();
$this->assertEqual(count($names), 2, 'Correct number of records found withNOT NULL age.');
$this->assertEqual($names[0], 'Gonzo', 'Correct record returned for NOT NULL age.');
$this->assertEqual($names[1], 'Kermit', 'Correct record returned for NOT NULL age.');
}
/**
* Tests that we can UNION multiple Select queries together.
*
* This is semantically equal to UNION DISTINCT, so we don't explicitly test
* that.
*/
function testUnion() {
$query_1 = db_select('test', 't')
->fields('t', array('name'))
->condition('age', array(27, 28), 'IN');
$query_2 = db_select('test', 't')
->fields('t', array('name'))
->condition('age', 28);
$query_1->union($query_2);
$names = $query_1->execute()->fetchCol();
// Ensure we only get 2 records.
$this->assertEqual(count($names), 2, 'UNION correctly discarded duplicates.');
$this->assertEqual($names[0], 'George', 'First query returned correct name.');
$this->assertEqual($names[1], 'Ringo', 'Second query returned correct name.');
}
/**
* Tests that we can UNION ALL multiple SELECT queries together.
*/
function testUnionAll() {
$query_1 = db_select('test', 't')
->fields('t', array('name'))
->condition('age', array(27, 28), 'IN');
$query_2 = db_select('test', 't')
->fields('t', array('name'))
->condition('age', 28);
$query_1->union($query_2, 'ALL');
$names = $query_1->execute()->fetchCol();
// Ensure we get all 3 records.
$this->assertEqual(count($names), 3, 'UNION ALL correctly preserved duplicates.');
$this->assertEqual($names[0], 'George', 'First query returned correct first name.');
$this->assertEqual($names[1], 'Ringo', 'Second query returned correct second name.');
$this->assertEqual($names[2], 'Ringo', 'Third query returned correct name.');
}
/**
* Tests that we can get a count query for a UNION Select query.
*/
function testUnionCount() {
$query_1 = db_select('test', 't')
->fields('t', array('name', 'age'))
->condition('age', array(27, 28), 'IN');
$query_2 = db_select('test', 't')
->fields('t', array('name', 'age'))
->condition('age', 28);
$query_1->union($query_2, 'ALL');
$names = $query_1->execute()->fetchCol();
$query_3 = $query_1->countQuery();
$count = $query_3->execute()->fetchField();
// Ensure the counts match.
$this->assertEqual(count($names), $count, "The count query's result matched the number of rows in the UNION query.");
}
/**
* Tests that random ordering of queries works.
*
* We take the approach of testing the Drupal layer only, rather than trying
* to test that the database's random number generator actually produces
* random queries (which is very difficult to do without an unacceptable risk
* of the test failing by accident).
*
* Therefore, in this test we simply run the same query twice and assert that
* the two results are reordered versions of each other (as well as of the
* same query without the random ordering). It is reasonable to assume that
* if we run the same select query twice and the results are in a different
* order each time, the only way this could happen is if we have successfully
* triggered the database's random ordering functionality.
*/
function testRandomOrder() {
// Use 52 items, so the chance that this test fails by accident will be the
// same as the chance that a deck of cards will come out in the same order
// after shuffling it (in other words, nearly impossible).
$number_of_items = 52;
while (db_query("SELECT MAX(id) FROM {test}")->fetchField() < $number_of_items) {
db_insert('test')->fields(array('name' => $this->randomMachineName()))->execute();
}
// First select the items in order and make sure we get an ordered list.
$expected_ids = range(1, $number_of_items);
$ordered_ids = db_select('test', 't')
->fields('t', array('id'))
->range(0, $number_of_items)
->orderBy('id')
->execute()
->fetchCol();
$this->assertEqual($ordered_ids, $expected_ids, 'A query without random ordering returns IDs in the correct order.');
// Now perform the same query, but instead choose a random ordering. We
// expect this to contain a differently ordered version of the original
// result.
$randomized_ids = db_select('test', 't')
->fields('t', array('id'))
->range(0, $number_of_items)
->orderRandom()
->execute()
->fetchCol();
$this->assertNotEqual($randomized_ids, $ordered_ids, 'A query with random ordering returns an unordered set of IDs.');
$sorted_ids = $randomized_ids;
sort($sorted_ids);
$this->assertEqual($sorted_ids, $ordered_ids, 'After sorting the random list, the result matches the original query.');
// Now perform the exact same query again, and make sure the order is
// different.
$randomized_ids_second_set = db_select('test', 't')
->fields('t', array('id'))
->range(0, $number_of_items)
->orderRandom()
->execute()
->fetchCol();
$this->assertNotEqual($randomized_ids_second_set, $randomized_ids, 'Performing the query with random ordering a second time returns IDs in a different order.');
$sorted_ids_second_set = $randomized_ids_second_set;
sort($sorted_ids_second_set);
$this->assertEqual($sorted_ids_second_set, $sorted_ids, 'After sorting the second random list, the result matches the sorted version of the first random list.');
}
/**
* Tests that filter by a regular expression works as expected.
*/
public function testRegexCondition() {
$test_groups[] = array(
'regex' => 'hn$',
'expected' => array(
'John',
),
);
$test_groups[] = array(
'regex' => '^Pau',
'expected' => array(
'Paul',
),
);
$test_groups[] = array(
'regex' => 'Ringo|George',
'expected' => array(
'Ringo', 'George',
),
);
$database = $this->container->get('database');
foreach ($test_groups as $test_group) {
$query = $database->select('test', 't');
$query->addField('t', 'name');
$query->condition('t.name', $test_group['regex'], 'REGEXP');
$result = $query->execute()->fetchCol();
$this->assertEqual(count($result), count($test_group['expected']), 'Returns the expected number of rows.');
$this->assertEqual(sort($result), sort($test_group['expected']), 'Returns the expected rows.');
}
// Ensure that filter by "#" still works due to the quoting.
$database->insert('test')
->fields(array(
'name' => 'Pete',
'age' => 26,
'job' => '#Drummer',
))
->execute();
$test_groups = array();
$test_groups[] = array(
'regex' => '#Drummer',
'expected' => array(
'Pete',
),
);
$test_groups[] = array(
'regex' => '#Singer',
'expected' => array(
),
);
foreach ($test_groups as $test_group) {
$query = $database->select('test', 't');
$query->addField('t', 'name');
$query->condition('t.job', $test_group['regex'], 'REGEXP');
$result = $query->execute()->fetchCol();
$this->assertEqual(count($result), count($test_group['expected']), 'Returns the expected number of rows.');
$this->assertEqual(sort($result), sort($test_group['expected']), 'Returns the expected rows.');
}
}
/**
* Tests that aliases are renamed when they are duplicates.
*/
function testSelectDuplicateAlias() {
$query = db_select('test', 't');
$alias1 = $query->addField('t', 'name', 'the_alias');
$alias2 = $query->addField('t', 'age', 'the_alias');
$this->assertNotIdentical($alias1, $alias2, 'Duplicate aliases are renamed.');
}
/**
* Tests that an invalid merge query throws an exception.
*/
function testInvalidSelectCount() {
try {
// This query will fail because the table does not exist.
// Normally it would throw an exception but we are suppressing
// it with the throw_exception option.
$options['throw_exception'] = FALSE;
db_select('some_table_that_doesnt_exist', 't', $options)
->fields('t')
->countQuery()
->execute();
$this->pass('$options[\'throw_exception\'] is FALSE, no Exception thrown.');
}
catch (\Exception $e) {
$this->fail('$options[\'throw_exception\'] is FALSE, but Exception thrown for invalid query.');
return;
}
try {
// This query will fail because the table does not exist.
db_select('some_table_that_doesnt_exist', 't')
->fields('t')
->countQuery()
->execute();
}
catch (\Exception $e) {
$this->pass('Exception thrown for invalid query.');
return;
}
$this->fail('No Exception thrown.');
}
/**
* Tests thrown exception for IN query conditions with an empty array.
*/
function testEmptyInCondition() {
try {
db_select('test', 't')
->fields('t')
->condition('age', array(), 'IN')
->execute();
$this->fail('Expected exception not thrown');
}
catch (InvalidQueryException $e) {
$this->assertEqual("Query condition 'age IN ()' cannot be empty.", $e->getMessage());
}
try {
db_select('test', 't')
->fields('t')
->condition('age', array(), 'NOT IN')
->execute();
$this->fail('Expected exception not thrown');
}
catch (InvalidQueryException $e) {
$this->assertEqual("Query condition 'age NOT IN ()' cannot be empty.", $e->getMessage());
}
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\SerializeQueryTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests serializing and unserializing a query.
*
* @group Database
*/
class SerializeQueryTest extends DatabaseTestBase {
/**
* Confirms that a query can be serialized and unserialized.
*/
function testSerializeQuery() {
$query = db_select('test');
$query->addField('test', 'age');
$query->condition('name', 'Ringo');
// If this doesn't work, it will throw an exception, so no need for an
// assertion.
$query = unserialize(serialize($query));
$results = $query->execute()->fetchCol();
$this->assertEqual($results[0], 28, 'Query properly executed after unserialization.');
}
}

View file

@ -0,0 +1,132 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\TaggingTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the tagging capabilities of the Select builder.
*
* Tags are a way to flag queries for alter hooks so they know
* what type of query it is, such as "node_access".
*
* @group Database
*/
class TaggingTest extends DatabaseTestBase {
/**
* Confirms that a query has a tag added to it.
*/
function testHasTag() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('test');
$this->assertTrue($query->hasTag('test'), 'hasTag() returned true.');
$this->assertFalse($query->hasTag('other'), 'hasTag() returned false.');
}
/**
* Tests query tagging "has all of these tags" functionality.
*/
function testHasAllTags() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('test');
$query->addTag('other');
$this->assertTrue($query->hasAllTags('test', 'other'), 'hasAllTags() returned true.');
$this->assertFalse($query->hasAllTags('test', 'stuff'), 'hasAllTags() returned false.');
}
/**
* Tests query tagging "has at least one of these tags" functionality.
*/
function testHasAnyTag() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('test');
$this->assertTrue($query->hasAnyTag('test', 'other'), 'hasAnyTag() returned true.');
$this->assertFalse($query->hasAnyTag('other', 'stuff'), 'hasAnyTag() returned false.');
}
/**
* Confirms that an extended query has a tag added to it.
*/
function testExtenderHasTag() {
$query = db_select('test')
->extend('Drupal\Core\Database\Query\SelectExtender');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('test');
$this->assertTrue($query->hasTag('test'), 'hasTag() returned true.');
$this->assertFalse($query->hasTag('other'), 'hasTag() returned false.');
}
/**
* Tests extended query tagging "has all of these tags" functionality.
*/
function testExtenderHasAllTags() {
$query = db_select('test')
->extend('Drupal\Core\Database\Query\SelectExtender');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('test');
$query->addTag('other');
$this->assertTrue($query->hasAllTags('test', 'other'), 'hasAllTags() returned true.');
$this->assertFalse($query->hasAllTags('test', 'stuff'), 'hasAllTags() returned false.');
}
/**
* Tests extended query tagging "has at least one of these tags" functionality.
*/
function testExtenderHasAnyTag() {
$query = db_select('test')
->extend('Drupal\Core\Database\Query\SelectExtender');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$query->addTag('test');
$this->assertTrue($query->hasAnyTag('test', 'other'), 'hasAnyTag() returned true.');
$this->assertFalse($query->hasAnyTag('other', 'stuff'), 'hasAnyTag() returned false.');
}
/**
* Tests that we can attach metadata to a query object.
*
* This is how we pass additional context to alter hooks.
*/
function testMetaData() {
$query = db_select('test');
$query->addField('test', 'name');
$query->addField('test', 'age', 'age');
$data = array(
'a' => 'A',
'b' => 'B',
);
$query->addMetaData('test', $data);
$return = $query->getMetaData('test');
$this->assertEqual($data, $return, 'Correct metadata returned.');
$return = $query->getMetaData('nothere');
$this->assertNull($return, 'Non-existent key returned NULL.');
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\TemporaryQueryTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the temporary query functionality.
*
* @group Database
*/
class TemporaryQueryTest extends DatabaseWebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('database_test');
/**
* Returns the number of rows of a table.
*/
function countTableRows($table_name) {
return db_select($table_name)->countQuery()->execute()->fetchField();
}
/**
* Confirms that temporary tables work and are limited to one request.
*/
function testTemporaryQuery() {
$this->drupalGet('database_test/db_query_temporary');
$data = json_decode($this->getRawContent());
if ($data) {
$this->assertEqual($this->countTableRows('test'), $data->row_count, 'The temporary table contains the correct amount of rows.');
$this->assertFalse(db_table_exists($data->table_name), 'The temporary table is, indeed, temporary.');
}
else {
$this->fail('The creation of the temporary table failed.');
}
// Now try to run two db_query_temporary() in the same request.
$table_name_test = db_query_temporary('SELECT name FROM {test}', array());
$table_name_task = db_query_temporary('SELECT pid FROM {test_task}', array());
$this->assertEqual($this->countTableRows($table_name_test), $this->countTableRows('test'), 'A temporary table was created successfully in this request.');
$this->assertEqual($this->countTableRows($table_name_task), $this->countTableRows('test_task'), 'A second temporary table was created successfully in this request.');
// Check that leading whitespace and comments do not cause problems
// in the modified query.
$sql = "
-- Let's select some rows into a temporary table
SELECT name FROM {test}
";
$table_name_test = db_query_temporary($sql, array());
$this->assertEqual($this->countTableRows($table_name_test), $this->countTableRows('test'), 'Leading white space and comments do not interfere with temporary table creation.');
}
}

View file

@ -0,0 +1,496 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\TransactionTest.
*/
namespace Drupal\system\Tests\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\TransactionOutOfOrderException;
use Drupal\Core\Database\TransactionNoActiveException;
/**
* Tests the transaction abstraction system.
*
* We test nesting by having two transaction layers, an outer and inner. The
* outer layer encapsulates the inner layer. Our transaction nesting abstraction
* should allow the outer layer function to call any function it wants,
* especially the inner layer that starts its own transaction, and be
* confident that, when the function it calls returns, its own transaction
* is still "alive."
*
* Call structure:
* transactionOuterLayer()
* Start transaction
* transactionInnerLayer()
* Start transaction (does nothing in database)
* [Maybe decide to roll back]
* Do more stuff
* Should still be in transaction A
*
* @group Database
*/
class TransactionTest extends DatabaseTestBase {
/**
* Encapsulates a transaction's "inner layer" with an "outer layer".
*
* This "outer layer" transaction starts and then encapsulates the "inner
* layer" transaction. This nesting is used to evaluate whether the database
* transaction API properly supports nesting. By "properly supports," we mean
* the outer transaction continues to exist regardless of what functions are
* called and whether those functions start their own transactions.
*
* In contrast, a typical database would commit the outer transaction, start
* a new transaction for the inner layer, commit the inner layer transaction,
* and then be confused when the outer layer transaction tries to commit its
* transaction (which was already committed when the inner transaction
* started).
*
* @param $suffix
* Suffix to add to field values to differentiate tests.
* @param $rollback
* Whether or not to try rolling back the transaction when we're done.
* @param $ddl_statement
* Whether to execute a DDL statement during the inner transaction.
*/
protected function transactionOuterLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) {
$connection = Database::getConnection();
$depth = $connection->transactionDepth();
$txn = db_transaction();
// Insert a single row into the testing table.
db_insert('test')
->fields(array(
'name' => 'David' . $suffix,
'age' => '24',
))
->execute();
$this->assertTrue($connection->inTransaction(), 'In transaction before calling nested transaction.');
// We're already in a transaction, but we call ->transactionInnerLayer
// to nest another transaction inside the current one.
$this->transactionInnerLayer($suffix, $rollback, $ddl_statement);
$this->assertTrue($connection->inTransaction(), 'In transaction after calling nested transaction.');
if ($rollback) {
// Roll back the transaction, if requested.
// This rollback should propagate to the last savepoint.
$txn->rollback();
$this->assertTrue(($connection->transactionDepth() == $depth), 'Transaction has rolled back to the last savepoint after calling rollback().');
}
}
/**
* Creates an "inner layer" transaction.
*
* This "inner layer" transaction is either used alone or nested inside of the
* "outer layer" transaction.
*
* @param $suffix
* Suffix to add to field values to differentiate tests.
* @param $rollback
* Whether or not to try rolling back the transaction when we're done.
* @param $ddl_statement
* Whether to execute a DDL statement during the transaction.
*/
protected function transactionInnerLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) {
$connection = Database::getConnection();
$depth = $connection->transactionDepth();
// Start a transaction. If we're being called from ->transactionOuterLayer,
// then we're already in a transaction. Normally, that would make starting
// a transaction here dangerous, but the database API handles this problem
// for us by tracking the nesting and avoiding the danger.
$txn = db_transaction();
$depth2 = $connection->transactionDepth();
$this->assertTrue($depth < $depth2, 'Transaction depth is has increased with new transaction.');
// Insert a single row into the testing table.
db_insert('test')
->fields(array(
'name' => 'Daniel' . $suffix,
'age' => '19',
))
->execute();
$this->assertTrue($connection->inTransaction(), 'In transaction inside nested transaction.');
if ($ddl_statement) {
$table = array(
'fields' => array(
'id' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
),
'primary key' => array('id'),
);
db_create_table('database_test_1', $table);
$this->assertTrue($connection->inTransaction(), 'In transaction inside nested transaction.');
}
if ($rollback) {
// Roll back the transaction, if requested.
// This rollback should propagate to the last savepoint.
$txn->rollback();
$this->assertTrue(($connection->transactionDepth() == $depth), 'Transaction has rolled back to the last savepoint after calling rollback().');
}
}
/**
* Tests transaction rollback on a database that supports transactions.
*
* If the active connection does not support transactions, this test does
* nothing.
*/
function testTransactionRollBackSupported() {
// This test won't work right if transactions are not supported.
if (!Database::getConnection()->supportsTransactions()) {
return;
}
try {
// Create two nested transactions. Roll back from the inner one.
$this->transactionOuterLayer('B', TRUE);
// Neither of the rows we inserted in the two transaction layers
// should be present in the tables post-rollback.
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DavidB'))->fetchField();
$this->assertNotIdentical($saved_age, '24', 'Cannot retrieve DavidB row after commit.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DanielB'))->fetchField();
$this->assertNotIdentical($saved_age, '19', 'Cannot retrieve DanielB row after commit.');
}
catch (\Exception $e) {
$this->fail($e->getMessage());
}
}
/**
* Tests transaction rollback on a database that doesn't support transactions.
*
* If the active driver supports transactions, this test does nothing.
*/
function testTransactionRollBackNotSupported() {
// This test won't work right if transactions are supported.
if (Database::getConnection()->supportsTransactions()) {
return;
}
try {
// Create two nested transactions. Attempt to roll back from the inner one.
$this->transactionOuterLayer('B', TRUE);
// Because our current database claims to not support transactions,
// the inserted rows should be present despite the attempt to roll back.
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DavidB'))->fetchField();
$this->assertIdentical($saved_age, '24', 'DavidB not rolled back, since transactions are not supported.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DanielB'))->fetchField();
$this->assertIdentical($saved_age, '19', 'DanielB not rolled back, since transactions are not supported.');
}
catch (\Exception $e) {
$this->fail($e->getMessage());
}
}
/**
* Tests a committed transaction.
*
* The behavior of this test should be identical for connections that support
* transactions and those that do not.
*/
function testCommittedTransaction() {
try {
// Create two nested transactions. The changes should be committed.
$this->transactionOuterLayer('A');
// Because we committed, both of the inserted rows should be present.
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DavidA'))->fetchField();
$this->assertIdentical($saved_age, '24', 'Can retrieve DavidA row after commit.');
$saved_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'DanielA'))->fetchField();
$this->assertIdentical($saved_age, '19', 'Can retrieve DanielA row after commit.');
}
catch (\Exception $e) {
$this->fail($e->getMessage());
}
}
/**
* Tests the compatibility of transactions with DDL statements.
*/
function testTransactionWithDdlStatement() {
// First, test that a commit works normally, even with DDL statements.
$transaction = db_transaction();
$this->insertRow('row');
$this->executeDDLStatement();
unset($transaction);
$this->assertRowPresent('row');
// Even in different order.
$this->cleanUp();
$transaction = db_transaction();
$this->executeDDLStatement();
$this->insertRow('row');
unset($transaction);
$this->assertRowPresent('row');
// Even with stacking.
$this->cleanUp();
$transaction = db_transaction();
$transaction2 = db_transaction();
$this->executeDDLStatement();
unset($transaction2);
$transaction3 = db_transaction();
$this->insertRow('row');
unset($transaction3);
unset($transaction);
$this->assertRowPresent('row');
// A transaction after a DDL statement should still work the same.
$this->cleanUp();
$transaction = db_transaction();
$transaction2 = db_transaction();
$this->executeDDLStatement();
unset($transaction2);
$transaction3 = db_transaction();
$this->insertRow('row');
$transaction3->rollback();
unset($transaction3);
unset($transaction);
$this->assertRowAbsent('row');
// The behavior of a rollback depends on the type of database server.
if (Database::getConnection()->supportsTransactionalDDL()) {
// For database servers that support transactional DDL, a rollback
// of a transaction including DDL statements should be possible.
$this->cleanUp();
$transaction = db_transaction();
$this->insertRow('row');
$this->executeDDLStatement();
$transaction->rollback();
unset($transaction);
$this->assertRowAbsent('row');
// Including with stacking.
$this->cleanUp();
$transaction = db_transaction();
$transaction2 = db_transaction();
$this->executeDDLStatement();
unset($transaction2);
$transaction3 = db_transaction();
$this->insertRow('row');
unset($transaction3);
$transaction->rollback();
unset($transaction);
$this->assertRowAbsent('row');
}
else {
// For database servers that do not support transactional DDL,
// the DDL statement should commit the transaction stack.
$this->cleanUp();
$transaction = db_transaction();
$this->insertRow('row');
$this->executeDDLStatement();
// Rollback the outer transaction.
try {
$transaction->rollback();
unset($transaction);
// @TODO: an exception should be triggered here, but is not, because
// "ROLLBACK" fails silently in MySQL if there is no transaction active.
// $this->fail(t('Rolling back a transaction containing DDL should fail.'));
}
catch (TransactionNoActiveException $e) {
$this->pass('Rolling back a transaction containing DDL should fail.');
}
$this->assertRowPresent('row');
}
}
/**
* Inserts a single row into the testing table.
*/
protected function insertRow($name) {
db_insert('test')
->fields(array(
'name' => $name,
))
->execute();
}
/**
* Executes a DDL statement.
*/
protected function executeDDLStatement() {
static $count = 0;
$table = array(
'fields' => array(
'id' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
),
'primary key' => array('id'),
);
db_create_table('database_test_' . ++$count, $table);
}
/**
* Starts over for a new test.
*/
protected function cleanUp() {
db_truncate('test')
->execute();
}
/**
* Asserts that a given row is present in the test table.
*
* @param $name
* The name of the row.
* @param $message
* The message to log for the assertion.
*/
function assertRowPresent($name, $message = NULL) {
if (!isset($message)) {
$message = format_string('Row %name is present.', array('%name' => $name));
}
$present = (boolean) db_query('SELECT 1 FROM {test} WHERE name = :name', array(':name' => $name))->fetchField();
return $this->assertTrue($present, $message);
}
/**
* Asserts that a given row is absent from the test table.
*
* @param $name
* The name of the row.
* @param $message
* The message to log for the assertion.
*/
function assertRowAbsent($name, $message = NULL) {
if (!isset($message)) {
$message = format_string('Row %name is absent.', array('%name' => $name));
}
$present = (boolean) db_query('SELECT 1 FROM {test} WHERE name = :name', array(':name' => $name))->fetchField();
return $this->assertFalse($present, $message);
}
/**
* Tests transaction stacking, commit, and rollback.
*/
function testTransactionStacking() {
// This test won't work right if transactions are not supported.
if (!Database::getConnection()->supportsTransactions()) {
return;
}
$database = Database::getConnection();
// Standard case: pop the inner transaction before the outer transaction.
$transaction = db_transaction();
$this->insertRow('outer');
$transaction2 = db_transaction();
$this->insertRow('inner');
// Pop the inner transaction.
unset($transaction2);
$this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the inner transaction');
// Pop the outer transaction.
unset($transaction);
$this->assertFalse($database->inTransaction(), 'Transaction closed after popping the outer transaction');
$this->assertRowPresent('outer');
$this->assertRowPresent('inner');
// Pop the transaction in a different order they have been pushed.
$this->cleanUp();
$transaction = db_transaction();
$this->insertRow('outer');
$transaction2 = db_transaction();
$this->insertRow('inner');
// Pop the outer transaction, nothing should happen.
unset($transaction);
$this->insertRow('inner-after-outer-commit');
$this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction');
// Pop the inner transaction, the whole transaction should commit.
unset($transaction2);
$this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction');
$this->assertRowPresent('outer');
$this->assertRowPresent('inner');
$this->assertRowPresent('inner-after-outer-commit');
// Rollback the inner transaction.
$this->cleanUp();
$transaction = db_transaction();
$this->insertRow('outer');
$transaction2 = db_transaction();
$this->insertRow('inner');
// Now rollback the inner transaction.
$transaction2->rollback();
unset($transaction2);
$this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction');
// Pop the outer transaction, it should commit.
$this->insertRow('outer-after-inner-rollback');
unset($transaction);
$this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction');
$this->assertRowPresent('outer');
$this->assertRowAbsent('inner');
$this->assertRowPresent('outer-after-inner-rollback');
// Rollback the inner transaction after committing the outer one.
$this->cleanUp();
$transaction = db_transaction();
$this->insertRow('outer');
$transaction2 = db_transaction();
$this->insertRow('inner');
// Pop the outer transaction, nothing should happen.
unset($transaction);
$this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction');
// Now rollback the inner transaction, it should rollback.
$transaction2->rollback();
unset($transaction2);
$this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction');
$this->assertRowPresent('outer');
$this->assertRowAbsent('inner');
// Rollback the outer transaction while the inner transaction is active.
// In that case, an exception will be triggered because we cannot
// ensure that the final result will have any meaning.
$this->cleanUp();
$transaction = db_transaction();
$this->insertRow('outer');
$transaction2 = db_transaction();
$this->insertRow('inner');
$transaction3 = db_transaction();
$this->insertRow('inner2');
// Rollback the outer transaction.
try {
$transaction->rollback();
unset($transaction);
$this->fail('Rolling back the outer transaction while the inner transaction is active resulted in an exception.');
}
catch (TransactionOutOfOrderException $e) {
$this->pass('Rolling back the outer transaction while the inner transaction is active resulted in an exception.');
}
$this->assertFalse($database->inTransaction(), 'No more in a transaction after rolling back the outer transaction');
// Try to commit one inner transaction.
unset($transaction3);
$this->pass('Trying to commit an inner transaction resulted in an exception.');
// Try to rollback one inner transaction.
try {
$transaction->rollback();
unset($transaction2);
$this->fail('Trying to commit an inner transaction resulted in an exception.');
}
catch (TransactionNoActiveException $e) {
$this->pass('Trying to commit an inner transaction resulted in an exception.');
}
$this->assertRowAbsent('outer');
$this->assertRowAbsent('inner');
$this->assertRowAbsent('inner2');
}
}

View file

@ -0,0 +1,150 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\UpdateComplexTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the Update query builder, complex queries.
*
* @group Database
*/
class UpdateComplexTest extends DatabaseTestBase {
/**
* Tests updates with OR conditionals.
*/
function testOrConditionUpdate() {
$update = db_update('test')
->fields(array('job' => 'Musician'))
->condition(db_or()
->condition('name', 'John')
->condition('name', 'Paul')
);
$num_updated = $update->execute();
$this->assertIdentical($num_updated, 2, 'Updated 2 records.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '2', 'Updated fields successfully.');
}
/**
* Tests WHERE IN clauses.
*/
function testInConditionUpdate() {
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->condition('name', array('John', 'Paul'), 'IN')
->execute();
$this->assertIdentical($num_updated, 2, 'Updated 2 records.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '2', 'Updated fields successfully.');
}
/**
* Tests WHERE NOT IN clauses.
*/
function testNotInConditionUpdate() {
// The o is lowercase in the 'NoT IN' operator, to make sure the operators
// work in mixed case.
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->condition('name', array('John', 'Paul', 'George'), 'NoT IN')
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '1', 'Updated fields successfully.');
}
/**
* Tests BETWEEN conditional clauses.
*/
function testBetweenConditionUpdate() {
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->condition('age', array(25, 26), 'BETWEEN')
->execute();
$this->assertIdentical($num_updated, 2, 'Updated 2 records.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '2', 'Updated fields successfully.');
}
/**
* Tests LIKE conditionals.
*/
function testLikeConditionUpdate() {
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->condition('name', '%ge%', 'LIKE')
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '1', 'Updated fields successfully.');
}
/**
* Tests UPDATE with expression values.
*/
function testUpdateExpression() {
$before_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchField();
$GLOBALS['larry_test'] = 1;
$num_updated = db_update('test')
->condition('name', 'Ringo')
->fields(array('job' => 'Musician'))
->expression('age', 'age + :age', array(':age' => 4))
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '1', 'Updated fields successfully.');
$person = db_query('SELECT * FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetch();
$this->assertEqual($person->name, 'Ringo', 'Name set correctly.');
$this->assertEqual($person->age, $before_age + 4, 'Age set correctly.');
$this->assertEqual($person->job, 'Musician', 'Job set correctly.');
$GLOBALS['larry_test'] = 0;
}
/**
* Tests UPDATE with only expression values.
*/
function testUpdateOnlyExpression() {
$before_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchField();
$num_updated = db_update('test')
->condition('name', 'Ringo')
->expression('age', 'age + :age', array(':age' => 4))
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$after_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchField();
$this->assertEqual($before_age + 4, $after_age, 'Age updated correctly');
}
/**
* Test UPDATE with a subselect value.
*/
function testSubSelectUpdate() {
$subselect = db_select('test_task', 't');
$subselect->addExpression('MAX(priority) + :increment', 'max_priority', array(':increment' => 30));
// Clone this to make sure we are running a different query when
// asserting.
$select = clone $subselect;
$query = db_update('test')
->expression('age', $subselect)
->condition('name', 'Ringo');
// Save the number of rows that updated for assertion later.
$num_updated = $query->execute();
$after_age = db_query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'Ringo'))->fetchField();
$expected_age = $select->execute()->fetchField();
$this->assertEqual($after_age, $expected_age);
$this->assertEqual(1, $num_updated, t('Expected 1 row to be updated in subselect update query.'));
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\UpdateLobTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the Update query builder with LOB fields.
*
* @group Database
*/
class UpdateLobTest extends DatabaseTestBase {
/**
* Confirms that we can update a blob column.
*/
function testUpdateOneBlob() {
$data = "This is\000a test.";
$this->assertTrue(strlen($data) === 15, 'Test data contains a NULL.');
$id = db_insert('test_one_blob')
->fields(array('blob1' => $data))
->execute();
$data .= $data;
db_update('test_one_blob')
->condition('id', $id)
->fields(array('blob1' => $data))
->execute();
$r = db_query('SELECT * FROM {test_one_blob} WHERE id = :id', array(':id' => $id))->fetchAssoc();
$this->assertTrue($r['blob1'] === $data, format_string('Can update a blob: id @id, @data.', array('@id' => $id, '@data' => serialize($r))));
}
/**
* Confirms that we can update two blob columns in the same table.
*/
function testUpdateMultipleBlob() {
$id = db_insert('test_two_blobs')
->fields(array(
'blob1' => 'This is',
'blob2' => 'a test',
))
->execute();
db_update('test_two_blobs')
->condition('id', $id)
->fields(array('blob1' => 'and so', 'blob2' => 'is this'))
->execute();
$r = db_query('SELECT * FROM {test_two_blobs} WHERE id = :id', array(':id' => $id))->fetchAssoc();
$this->assertTrue($r['blob1'] === 'and so' && $r['blob2'] === 'is this', 'Can update multiple blobs per row.');
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Database\UpdateTest.
*/
namespace Drupal\system\Tests\Database;
/**
* Tests the update query builder.
*
* @group Database
*/
class UpdateTest extends DatabaseTestBase {
/**
* Confirms that we can update a single record successfully.
*/
function testSimpleUpdate() {
$num_updated = db_update('test')
->fields(array('name' => 'Tiffany'))
->condition('id', 1)
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$saved_name = db_query('SELECT name FROM {test} WHERE id = :id', array(':id' => 1))->fetchField();
$this->assertIdentical($saved_name, 'Tiffany', 'Updated name successfully.');
}
/**
* Confirms updating to NULL.
*/
function testSimpleNullUpdate() {
$this->ensureSampleDataNull();
$num_updated = db_update('test_null')
->fields(array('age' => NULL))
->condition('name', 'Kermit')
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$saved_age = db_query('SELECT age FROM {test_null} WHERE name = :name', array(':name' => 'Kermit'))->fetchField();
$this->assertNull($saved_age, 'Updated name successfully.');
}
/**
* Confirms that we can update multiple records successfully.
*/
function testMultiUpdate() {
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->condition('job', 'Singer')
->execute();
$this->assertIdentical($num_updated, 2, 'Updated 2 records.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '2', 'Updated fields successfully.');
}
/**
* Confirms that we can update multiple records with a non-equality condition.
*/
function testMultiGTUpdate() {
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->condition('age', 26, '>')
->execute();
$this->assertIdentical($num_updated, 2, 'Updated 2 records.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '2', 'Updated fields successfully.');
}
/**
* Confirms that we can update multiple records with a where call.
*/
function testWhereUpdate() {
$num_updated = db_update('test')
->fields(array('job' => 'Musician'))
->where('age > :age', array(':age' => 26))
->execute();
$this->assertIdentical($num_updated, 2, 'Updated 2 records.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '2', 'Updated fields successfully.');
}
/**
* Confirms that we can stack condition and where calls.
*/
function testWhereAndConditionUpdate() {
$update = db_update('test')
->fields(array('job' => 'Musician'))
->where('age > :age', array(':age' => 26))
->condition('name', 'Ringo');
$num_updated = $update->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$num_matches = db_query('SELECT COUNT(*) FROM {test} WHERE job = :job', array(':job' => 'Musician'))->fetchField();
$this->assertIdentical($num_matches, '1', 'Updated fields successfully.');
}
/**
* Tests updating with expressions.
*/
function testExpressionUpdate() {
// Ensure that expressions are handled properly. This should set every
// record's age to a square of itself.
$num_rows = db_update('test')
->expression('age', 'age * age')
->execute();
$this->assertIdentical($num_rows, 4, 'Updated 4 records.');
$saved_name = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => pow(26, 2)))->fetchField();
$this->assertIdentical($saved_name, 'Paul', 'Successfully updated values using an algebraic expression.');
}
/**
* Tests return value on update.
*/
function testUpdateAffectedRows() {
// At 5am in the morning, all band members but those with a priority 1 task
// are sleeping. So we set their tasks to 'sleep'. 5 records match the
// condition and therefore are affected by the query, even though two of
// them actually don't have to be changed because their value was already
// 'sleep'. Still, execute() should return 5 affected rows, not only 3,
// because that's cross-db expected behavior.
$num_rows = db_update('test_task')
->condition('priority', 1, '<>')
->fields(array('task' => 'sleep'))
->execute();
$this->assertIdentical($num_rows, 5, 'Correctly returned 5 affected rows.');
}
/**
* Confirm that we can update the primary key of a record successfully.
*/
function testPrimaryKeyUpdate() {
$num_updated = db_update('test')
->fields(array('id' => 42, 'name' => 'John'))
->condition('id', 1)
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 record.');
$saved_name= db_query('SELECT name FROM {test} WHERE id = :id', array(':id' => 42))->fetchField();
$this->assertIdentical($saved_name, 'John', 'Updated primary key successfully.');
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Datetime\DrupalDateTimeTest.
*/
namespace Drupal\system\Tests\Datetime;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\simpletest\WebTestBase;
use Drupal\user\Entity\User;
/**
* Tests DrupalDateTime functionality.
*
* @group Datetime
*/
class DrupalDateTimeTest extends WebTestBase {
/**
* Set up required modules.
*/
public static $modules = array();
/**
* Test setup.
*/
protected function setUp() {
parent::setUp();
}
/**
* Test that the AJAX Timezone Callback can deal with various formats.
*/
public function testSystemTimezone() {
$options = array(
'query' => array(
'date' => 'Tue+Sep+17+2013+21%3A35%3A31+GMT%2B0100+(BST)#',
)
);
// Query the AJAX Timezone Callback with a long-format date.
$response = $this->drupalGet('system/timezone/BST/3600/1', $options);
$this->assertEqual($response, '"Europe\/London"', 'Timezone AJAX callback successfully identifies and responds to a long-format date.');
}
/**
* Test that DrupalDateTime can detect the right timezone to use.
* Test with a variety of less commonly used timezone names to
* help ensure that the system timezone will be different than the
* stated timezones.
*/
public function testDateTimezone() {
$date_string = '2007-01-31 21:00:00';
// Make sure no site timezone has been set.
$this->config('system.date')
->set('timezone.user.configurable', 0)
->set('timezone.default', NULL)
->save();
// Detect the system timezone.
$system_timezone = date_default_timezone_get();
// Create a date object with an unspecified timezone, which should
// end up using the system timezone.
$date = new DrupalDateTime($date_string);
$timezone = $date->getTimezone()->getName();
$this->assertTrue($timezone == $system_timezone, 'DrupalDateTime uses the system timezone when there is no site timezone.');
// Create a date object with a specified timezone.
$date = new DrupalDateTime($date_string, 'America/Yellowknife');
$timezone = $date->getTimezone()->getName();
$this->assertTrue($timezone == 'America/Yellowknife', 'DrupalDateTime uses the specified timezone if provided.');
// Set a site timezone.
$this->config('system.date')->set('timezone.default', 'Europe/Warsaw')->save();
// Create a date object with an unspecified timezone, which should
// end up using the site timezone.
$date = new DrupalDateTime($date_string);
$timezone = $date->getTimezone()->getName();
$this->assertTrue($timezone == 'Europe/Warsaw', 'DrupalDateTime uses the site timezone if provided.');
// Create user.
$this->config('system.date')->set('timezone.user.configurable', 1)->save();
$test_user = $this->drupalCreateUser(array());
$this->drupalLogin($test_user);
// Set up the user with a different timezone than the site.
$edit = array('mail' => $test_user->getEmail(), 'timezone' => 'Asia/Manila');
$this->drupalPostForm('user/' . $test_user->id() . '/edit', $edit, t('Save'));
// Reload the user and reset the timezone in AccountProxy::setAccount().
\Drupal::entityManager()->getStorage('user')->resetCache();
$this->container->get('current_user')->setAccount(User::load($test_user->id()));
// Create a date object with an unspecified timezone, which should
// end up using the user timezone.
$date = new DrupalDateTime($date_string);
$timezone = $date->getTimezone()->getName();
$this->assertTrue($timezone == 'Asia/Manila', 'DrupalDateTime uses the user timezone, if configurable timezones are used and it is set.');
}
/**
* Tests the ability to override the time zone in the format method.
*/
function testTimezoneFormat() {
// Create a date in UTC
$date = DrupalDateTime::createFromTimestamp(87654321, 'UTC');
// Verify that the date format method displays the default time zone.
$this->assertEqual($date->format('Y/m/d H:i:s e'), '1972/10/11 12:25:21 UTC', 'Date has default UTC time zone and correct date/time.');
// Verify that the format method can override the time zone.
$this->assertEqual($date->format('Y/m/d H:i:s e', array('timezone' => 'America/New_York')), '1972/10/11 08:25:21 America/New_York', 'Date displayed overidden time zone and correct date/time');
// Verify that the date format method still displays the default time zone
// for the date object.
$this->assertEqual($date->format('Y/m/d H:i:s e'), '1972/10/11 12:25:21 UTC', 'Date still has default UTC time zone and correct date/time');
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\DrupalKernel\ContainerRebuildWebTest.
*/
namespace Drupal\system\Tests\DrupalKernel;
use Drupal\simpletest\WebTestBase;
/**
* Ensures that the container rebuild works as expected.
*
* @group DrupalKernel
*/
class ContainerRebuildWebTest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['service_provider_test'];
/**
* Sets a different deployment identifier.
*/
public function testSetContainerRebuildWithDifferentDeploymentIdentifier() {
$this->drupalGet('<front>');
$this->assertHeader('container_rebuild_indicator', FALSE);
$this->writeSettings(['settings' => ['deployment_identifier' => (object) ['value' => 'new-identifier', 'required' => TRUE]]]);
$this->drupalGet('<front>');
$this->assertHeader('container_rebuild_indicator', 'new-identifier');
}
}

View file

@ -0,0 +1,50 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\DrupalKernel\ContentNegotiationTest.
*/
namespace Drupal\system\Tests\DrupalKernel;
use Drupal\simpletest\WebTestBase;
/**
* Tests content negotiation.
*
* @group DrupalKernel
*/
class ContentNegotiationTest extends WebTestBase {
/**
* Verifies HTML responses for bogus Accept headers.
*
* Drupal does not fully support older browsers, but a page output is still
* expected.
*
* @see https://www.drupal.org/node/1716790
*/
function testBogusAcceptHeader() {
$tests = array(
// See https://bugs.webkit.org/show_bug.cgi?id=27267.
'Firefox 3.5 (2009)' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'IE8 (2009)' => 'image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, */*',
'Opera (2009)' => 'text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1',
'Chrome (2009)' => 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
// See https://github.com/symfony/symfony/pull/564.
'Firefox 3.6 (2010)' => 'text/html,application/xhtml+xml,application/json,application/xml;q=0.9,*/*;q=0.8',
'Safari (2010)' => '*/*',
'Opera (2010)' => 'image/jpeg,image/gif,image/x-xbitmap,text/html,image/webp,image/png,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.1',
// See https://www.drupal.org/node/1716790.
'Safari (2010), iOS 4.2.1 (2012)' => 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
'Android #1 (2012)' => 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
'Android #2 (2012)' => 'text/xml,text/html,application/xhtml+xml,image/png,text/plain,*/*;q=0.8',
);
foreach ($tests as $case => $header) {
$this->drupalGet('', array(), array('Accept: ' . $header));
$this->assertNoText('Unsupported Media Type', '"Unsupported Media Type" not found for ' . $case);
$this->assertText(t('Log in'), '"Log in" found for ' . $case);
}
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\DrupalKernel\DrupalKernelSiteTest.
*/
namespace Drupal\system\Tests\DrupalKernel;
use Drupal\Core\Site\Settings;
use Drupal\simpletest\KernelTestBase;
/**
* Tests site-specific service overrides.
*
* @group DrupalKernel
*/
class DrupalKernelSiteTest extends KernelTestBase {
/**
* Tests services.yml in site directory.
*/
public function testServicesYml() {
$container_yamls = Settings::get('container_yamls');
$container_yamls[] = $this->siteDirectory . '/services.yml';
$this->settingsSet('container_yamls', $container_yamls);
$this->assertFalse($this->container->has('site.service.yml'));
// A service provider class always has precedence over services.yml files.
// KernelTestBase::buildContainer() swaps out many services with in-memory
// implementations already, so those cannot be tested.
$this->assertIdentical(get_class($this->container->get('cache.backend.database')), 'Drupal\Core\Cache\DatabaseBackendFactory');
$class = __CLASS__;
$doc = <<<EOD
services:
# Add a new service.
site.service.yml:
class: $class
# Swap out a core service.
cache.backend.database:
class: Drupal\Core\Cache\MemoryBackendFactory
EOD;
file_put_contents($this->siteDirectory . '/services.yml', $doc);
// Rebuild the container.
$this->container->get('kernel')->rebuildContainer();
$this->assertTrue($this->container->has('site.service.yml'));
$this->assertIdentical(get_class($this->container->get('cache.backend.database')), 'Drupal\Core\Cache\MemoryBackendFactory');
}
}

View file

@ -0,0 +1,214 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\DrupalKernel\DrupalKernelTest.
*/
namespace Drupal\system\Tests\DrupalKernel;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests DIC compilation to disk.
*
* @group DrupalKernel
*/
class DrupalKernelTest extends KernelTestBase {
protected function setUp() {
// DrupalKernel relies on global $config_directories and requires those
// directories to exist. Therefore, create the directories, but do not
// invoke KernelTestBase::setUp(), since that would set up further
// environment aspects, which would distort this test, because it tests
// the DrupalKernel (re-)building itself.
$this->prepareConfigDirectories();
$this->settingsSet('php_storage', array('service_container' => array(
'bin' => 'service_container',
'class' => 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage',
'directory' => DRUPAL_ROOT . '/' . $this->publicFilesDirectory . '/php',
'secret' => Settings::getHashSalt(),
)));
}
/**
* {@inheritdoc}
*/
protected function prepareConfigDirectories() {
\Drupal::setContainer($this->originalContainer);
parent::prepareConfigDirectories();
\Drupal::unsetContainer();
}
/**
* Build a kernel for testings.
*
* Because the bootstrap is in DrupalKernel::boot and that involved loading
* settings from the filesystem we need to go to extra lengths to build a kernel
* for testing.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object to use in booting the kernel.
* @param array $modules_enabled
* A list of modules to enable on the kernel.
* @param bool $read_only
* Build the kernel in a read only state.
*
* @return \Drupal\Core\DrupalKernel
* New kernel for testing.
*/
protected function getTestKernel(Request $request, array $modules_enabled = NULL, $read_only = FALSE) {
// Manually create kernel to avoid replacing settings.
$class_loader = require DRUPAL_ROOT . '/autoload.php';
$kernel = DrupalKernel::createFromRequest($request, $class_loader, 'testing');
$this->settingsSet('container_yamls', []);
$this->settingsSet('hash_salt', $this->databasePrefix);
if (isset($modules_enabled)) {
$kernel->updateModules($modules_enabled);
}
$kernel->boot();
if ($read_only) {
$php_storage = Settings::get('php_storage');
$php_storage['service_container']['class'] = 'Drupal\Component\PhpStorage\FileReadOnlyStorage';
$this->settingsSet('php_storage', $php_storage);
}
return $kernel;
}
/**
* Tests DIC compilation.
*/
public function testCompileDIC() {
// @todo: write a memory based storage backend for testing.
$modules_enabled = array(
'system' => 'system',
'user' => 'user',
);
$request = Request::createFromGlobals();
$this->getTestKernel($request, $modules_enabled);
// Instantiate it a second time and we should get the compiled Container
// class.
$kernel = $this->getTestKernel($request);
$container = $kernel->getContainer();
$refClass = new \ReflectionClass($container);
$is_compiled_container =
$refClass->getParentClass()->getName() == 'Drupal\Core\DependencyInjection\Container' &&
!$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$this->assertTrue($is_compiled_container);
// Verify that the list of modules is the same for the initial and the
// compiled container.
$module_list = array_keys($container->get('module_handler')->getModuleList());
$this->assertEqual(array_values($modules_enabled), $module_list);
// Now use the read-only storage implementation, simulating a "production"
// environment.
$container = $this->getTestKernel($request, NULL, TRUE)
->getContainer();
$refClass = new \ReflectionClass($container);
$is_compiled_container =
$refClass->getParentClass()->getName() == 'Drupal\Core\DependencyInjection\Container' &&
!$refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$this->assertTrue($is_compiled_container);
// Verify that the list of modules is the same for the initial and the
// compiled container.
$module_list = array_keys($container->get('module_handler')->getModuleList());
$this->assertEqual(array_values($modules_enabled), $module_list);
// Test that our synthetic services are there.
$class_loader = $container->get('class_loader');
$refClass = new \ReflectionClass($class_loader);
$this->assertTrue($refClass->hasMethod('loadClass'), 'Container has a class loader');
// We make this assertion here purely to show that the new container below
// is functioning correctly, i.e. we get a brand new ContainerBuilder
// which has the required new services, after changing the list of enabled
// modules.
$this->assertFalse($container->has('service_provider_test_class'));
// Add another module so that we can test that the new module's bundle is
// registered to the new container.
$modules_enabled['service_provider_test'] = 'service_provider_test';
$this->getTestKernel($request, $modules_enabled, TRUE);
// Instantiate it a second time and we should still get a ContainerBuilder
// class because we are using the read-only PHP storage.
$kernel = $this->getTestKernel($request, $modules_enabled, TRUE);
$container = $kernel->getContainer();
$refClass = new \ReflectionClass($container);
$is_container_builder = $refClass->isSubclassOf('Symfony\Component\DependencyInjection\ContainerBuilder');
$this->assertTrue($is_container_builder, 'Container is a builder');
// Assert that the new module's bundle was registered to the new container.
$this->assertTrue($container->has('service_provider_test_class'), 'Container has test service');
// Test that our synthetic services are there.
$class_loader = $container->get('class_loader');
$refClass = new \ReflectionClass($class_loader);
$this->assertTrue($refClass->hasMethod('loadClass'), 'Container has a class loader');
// Check that the location of the new module is registered.
$modules = $container->getParameter('container.modules');
$this->assertEqual($modules['service_provider_test'], array(
'type' => 'module',
'pathname' => drupal_get_filename('module', 'service_provider_test'),
'filename' => NULL,
));
}
/**
* Tests repeated loading of compiled DIC with different environment.
*/
public function testRepeatedBootWithDifferentEnvironment() {
$request = Request::createFromGlobals();
$class_loader = require DRUPAL_ROOT . '/autoload.php';
$environments = [
'testing1',
'testing1',
'testing2',
'testing2',
];
foreach ($environments as $environment) {
$kernel = DrupalKernel::createFromRequest($request, $class_loader, $environment);
$this->settingsSet('container_yamls', []);
$this->settingsSet('hash_salt', $this->databasePrefix);
$kernel->boot();
}
$this->pass('Repeatedly loaded compiled DIC with different environment');
}
/**
* Tests setting of site path after kernel boot.
*/
public function testPreventChangeOfSitePath() {
// @todo: write a memory based storage backend for testing.
$modules_enabled = array(
'system' => 'system',
'user' => 'user',
);
$request = Request::createFromGlobals();
$kernel = $this->getTestKernel($request, $modules_enabled);
$pass = FALSE;
try {
$kernel->setSitePath('/dev/null');
}
catch (\LogicException $e) {
$pass = TRUE;
}
$this->assertTrue($pass, 'Throws LogicException if DrupalKernel::setSitePath() is called after boot');
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\DrupalKernel\ServiceDestructionTest.
*/
namespace Drupal\system\Tests\DrupalKernel;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
/**
* Tests that services are correctly destructed.
*
* @group DrupalKernel
*/
class ServiceDestructionTest extends KernelTestBase {
/**
* Verifies that services are destructed when used.
*/
public function testDestructionUsed() {
// Enable the test module to add it to the container.
$this->enableModules(array('service_provider_test'));
$request = $this->container->get('request_stack')->getCurrentRequest();
$kernel = $this->container->get('kernel');
$kernel->preHandle($request);
// The service has not been destructed yet.
$this->assertNull(\Drupal::state()->get('service_provider_test.destructed'));
// Call the class and then terminate the kernel
$this->container->get('service_provider_test_class');
$response = new Response();
$kernel->terminate($request, $response);
$this->assertTrue(\Drupal::state()->get('service_provider_test.destructed'));
}
/**
* Verifies that services are not unnecessarily destructed when not used.
*/
public function testDestructionUnused() {
// Enable the test module to add it to the container.
$this->enableModules(array('service_provider_test'));
$request = $this->container->get('request_stack')->getCurrentRequest();
$kernel = $this->container->get('kernel');
$kernel->preHandle($request);
// The service has not been destructed yet.
$this->assertNull(\Drupal::state()->get('service_provider_test.destructed'));
// Terminate the kernel. The test class has not been called, so it should not
// be destructed.
$response = new Response();
$kernel->terminate($request, $response);
$this->assertNull(\Drupal::state()->get('service_provider_test.destructed'));
}
}

View file

@ -0,0 +1,210 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Element\PathElementFormTest.
*/
namespace Drupal\system\Tests\Element;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\PathElement;
use Drupal\Core\Url;
use Drupal\simpletest\KernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
/**
* Tests PathElement validation and conversion functionality.
*
* @group Form
*/
class PathElementFormTest extends KernelTestBase implements FormInterface {
/**
* User for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $testUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'user');
/**
* Sets up the test.
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['router', 'sequences', 'key_value_expire']);
$this->installEntitySchema('user');
\Drupal::service('router.builder')->rebuild();
/** @var \Drupal\user\RoleInterface $role */
$role = Role::create(array(
'id' => 'admin',
'label' => 'admin',
));
$role->grantPermission('link to any page');
$role->save();
$this->testUser = User::create(array(
'name' => 'foobar',
'mail' => 'foobar@example.com',
));
$this->testUser->addRole($role->id());
$this->testUser->save();
\Drupal::service('current_user')->setAccount($this->testUser);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'test_path_element';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// A required validated path.
$form['required_validate'] = array(
'#type' => 'path',
'#required' => TRUE,
'#title' => 'required_validate',
'#convert_path' => PathElement::CONVERT_NONE,
);
// A non validated required path.
$form['required_non_validate'] = array(
'#type' => 'path',
'#required' => TRUE,
'#title' => 'required_non_validate',
'#convert_path' => PathElement::CONVERT_NONE,
'#validate_path' => FALSE,
);
// A non required validated path.
$form['optional_validate'] = array(
'#type' => 'path',
'#required' => FALSE,
'#title' => 'optional_validate',
'#convert_path' => PathElement::CONVERT_NONE,
);
// A non required converted path.
$form['optional_validate'] = array(
'#type' => 'path',
'#required' => FALSE,
'#title' => 'optional_validate',
'#convert_path' => PathElement::CONVERT_ROUTE,
);
// A converted required validated path.
$form['required_validate_route'] = array(
'#type' => 'path',
'#required' => TRUE,
'#title' => 'required_validate_route',
);
// A converted required validated path.
$form['required_validate_url'] = array(
'#type' => 'path',
'#required' => TRUE,
'#title' => 'required_validate_url',
'#convert_path' => PathElement::CONVERT_URL,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {}
/**
* Form validation handler.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function validateForm(array &$form, FormStateInterface $form_state) {}
/**
* Tests that default handlers are added even if custom are specified.
*/
public function testPathElement() {
$form_state = (new FormState())
->setValues([
'required_validate' => 'user/' . $this->testUser->id(),
'required_non_validate' => 'magic-ponies',
'required_validate_route' => 'user/' . $this->testUser->id(),
'required_validate_url' => 'user/' . $this->testUser->id(),
]);
$form_builder = $this->container->get('form_builder');
$form_builder->submitForm($this, $form_state);
// Valid form state.
$this->assertEqual(count($form_state->getErrors()), 0);
$this->assertEqual($form_state->getValue('required_validate_route'), array(
'route_name' => 'entity.user.canonical',
'route_parameters' => array(
'user' => $this->testUser->id(),
),
));
/** @var \Drupal\Core\Url $url */
$url = $form_state->getValue('required_validate_url');
$this->assertTrue($url instanceof Url);
$this->assertEqual($url->getRouteName(), 'entity.user.canonical');
$this->assertEqual($url->getRouteParameters(), array(
'user' => $this->testUser->id(),
));
// Test #required.
$form_state = (new FormState())
->setValues([
'required_non_validate' => 'magic-ponies',
'required_validate_route' => 'user/' . $this->testUser->id(),
'required_validate_url' => 'user/' . $this->testUser->id(),
]);
$form_builder->submitForm($this, $form_state);
$errors = $form_state->getErrors();
// Should be missing 'required_validate' field.
$this->assertEqual(count($errors), 1);
$this->assertEqual($errors, array('required_validate' => t('!name field is required.', array('!name' => 'required_validate'))));
// Test invalid parameters.
$form_state = (new FormState())
->setValues([
'required_validate' => 'user/74',
'required_non_validate' => 'magic-ponies',
'required_validate_route' => 'user/74',
'required_validate_url' => 'user/74',
]);
$form_builder = $this->container->get('form_builder');
$form_builder->submitForm($this, $form_state);
// Valid form state.
$errors = $form_state->getErrors();
$this->assertEqual(count($errors), 3);
$this->assertEqual($errors, array(
'required_validate' => t('This path does not exist or you do not have permission to link to %path.', array('%path' => 'user/74')),
'required_validate_route' => t('This path does not exist or you do not have permission to link to %path.', array('%path' => 'user/74')),
'required_validate_url' => t('This path does not exist or you do not have permission to link to %path.', array('%path' => 'user/74')),
));
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\BundleConstraintValidatorTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\simpletest\KernelTestBase;
use Drupal\system\Tests\TypedData;
/**
* Tests validation constraints for BundleConstraintValidator.
*
* @group Entity
*/
class BundleConstraintValidatorTest extends KernelTestBase {
/**
* The typed data manager to use.
*
* @var \Drupal\Core\TypedData\TypedDataManager
*/
protected $typedData;
public static $modules = array('node', 'field', 'text', 'user');
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->typedData = $this->container->get('typed_data_manager');
}
/**
* Tests bundle constraint validation.
*/
public function testValidation() {
// Test with multiple values.
$this->assertValidation(array('foo', 'bar'));
// Test with a single string value as well.
$this->assertValidation('foo');
}
/**
* Executes the BundleConstraintValidator test for a given bundle.
*
* @param string|array $bundle
* Bundle/bundles to use as constraint option.
*/
protected function assertValidation($bundle) {
// Create a typed data definition with a Bundle constraint.
$definition = DataDefinition::create('entity_reference')
->addConstraint('Bundle', $bundle);
// Test the validation.
$node = $this->container->get('entity.manager')->getStorage('node')->create(array('type' => 'foo'));
$typed_data = $this->typedData->create($definition, $node);
$violations = $typed_data->validate();
$this->assertEqual($violations->count(), 0, 'Validation passed for correct value.');
// Test the validation when an invalid value is passed.
$page_node = $this->container->get('entity.manager')->getStorage('node')->create(array('type' => 'baz'));
$typed_data = $this->typedData->create($definition, $page_node);
$violations = $typed_data->validate();
$this->assertEqual($violations->count(), 1, 'Validation failed for incorrect value.');
// Make sure the information provided by a violation is correct.
$violation = $violations[0];
$this->assertEqual($violation->getMessage(), t('The entity must be of bundle %bundle.', array('%bundle' => implode(', ', (array) $bundle))), 'The message for invalid value is correct.');
$this->assertEqual($violation->getRoot(), $typed_data, 'Violation root is correct.');
$this->assertEqual($violation->getInvalidValue(), $page_node, 'The invalid value is set correctly in the violation.');
}
}

View file

@ -0,0 +1,236 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\ConfigEntityImportTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\image\Entity\ImageStyle;
use Drupal\simpletest\WebTestBase;
/**
* Tests ConfigEntity importing.
*
* @group Entity
*/
class ConfigEntityImportTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('action', 'block', 'filter', 'image', 'search', 'search_extra_type');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
/**
* Runs test methods for each module within a single test run.
*/
public function testConfigUpdateImport() {
$this->doActionUpdate();
$this->doBlockUpdate();
$this->doFilterFormatUpdate();
$this->doImageStyleUpdate();
$this->doSearchPageUpdate();
}
/**
* Tests updating a action during import.
*/
protected function doActionUpdate() {
// Create a test action with a known label.
$name = 'system.action.apple';
$entity = entity_create('action', array(
'id' => 'apple',
'plugin' => 'action_message_action',
));
$entity->save();
$this->checkSinglePluginConfigSync($entity, 'configuration', 'message', '');
// Read the existing data, and prepare an altered version in staging.
$custom_data = $original_data = $this->container->get('config.storage')->read($name);
$custom_data['configuration']['message'] = 'Granny Smith';
$this->assertConfigUpdateImport($name, $original_data, $custom_data);
}
/**
* Tests updating a block during import.
*/
protected function doBlockUpdate() {
// Create a test block with a known label.
$name = 'block.block.apple';
$block = $this->drupalPlaceBlock('system_powered_by_block', array(
'id' => 'apple',
'label' => 'Red Delicious',
));
$this->checkSinglePluginConfigSync($block, 'settings', 'label', 'Red Delicious');
// Read the existing data, and prepare an altered version in staging.
$custom_data = $original_data = $this->container->get('config.storage')->read($name);
$custom_data['settings']['label'] = 'Granny Smith';
$this->assertConfigUpdateImport($name, $original_data, $custom_data);
}
/**
* Tests updating a filter format during import.
*/
protected function doFilterFormatUpdate() {
// Create a test filter format with a known label.
$name = 'filter.format.plain_text';
/** @var $entity \Drupal\filter\Entity\FilterFormat */
$entity = entity_load('filter_format', 'plain_text');
$plugin_collection = $entity->getPluginCollections()['filters'];
$filters = $entity->get('filters');
$this->assertIdentical(72, $filters['filter_url']['settings']['filter_url_length']);
$filters['filter_url']['settings']['filter_url_length'] = 100;
$entity->set('filters', $filters);
$entity->save();
$this->assertIdentical($filters, $entity->get('filters'));
$this->assertIdentical($filters, $plugin_collection->getConfiguration());
$filters['filter_url']['settings']['filter_url_length'] = -100;
$entity->getPluginCollections()['filters']->setConfiguration($filters);
$entity->save();
$this->assertIdentical($filters, $entity->get('filters'));
$this->assertIdentical($filters, $plugin_collection->getConfiguration());
// Read the existing data, and prepare an altered version in staging.
$custom_data = $original_data = $this->container->get('config.storage')->read($name);
$custom_data['filters']['filter_url']['settings']['filter_url_length'] = 100;
$this->assertConfigUpdateImport($name, $original_data, $custom_data);
}
/**
* Tests updating an image style during import.
*/
protected function doImageStyleUpdate() {
// Create a test image style with a known label.
$name = 'image.style.thumbnail';
/** @var $entity \Drupal\image\Entity\ImageStyle */
$entity = ImageStyle::load('thumbnail');
$plugin_collection = $entity->getPluginCollections()['effects'];
$effects = $entity->get('effects');
$effect_id = key($effects);
$this->assertIdentical(100, $effects[$effect_id]['data']['height']);
$effects[$effect_id]['data']['height'] = 50;
$entity->set('effects', $effects);
$entity->save();
// Ensure the entity and plugin have the correct configuration.
$this->assertIdentical($effects, $entity->get('effects'));
$this->assertIdentical($effects, $plugin_collection->getConfiguration());
$effects[$effect_id]['data']['height'] = -50;
$entity->getPluginCollections()['effects']->setConfiguration($effects);
$entity->save();
// Ensure the entity and plugin have the correct configuration.
$this->assertIdentical($effects, $entity->get('effects'));
$this->assertIdentical($effects, $plugin_collection->getConfiguration());
// Read the existing data, and prepare an altered version in staging.
$custom_data = $original_data = $this->container->get('config.storage')->read($name);
$effect_name = key($original_data['effects']);
$custom_data['effects'][$effect_name]['data']['upscale'] = FALSE;
$this->assertConfigUpdateImport($name, $original_data, $custom_data);
}
/**
* Tests updating a search page during import.
*/
protected function doSearchPageUpdate() {
// Create a test search page with a known label.
$name = 'search.page.apple';
$entity = entity_create('search_page', array(
'id' => 'apple',
'plugin' => 'search_extra_type_search',
));
$entity->save();
$this->checkSinglePluginConfigSync($entity, 'configuration', 'boost', 'bi');
// Read the existing data, and prepare an altered version in staging.
$custom_data = $original_data = $this->container->get('config.storage')->read($name);
$custom_data['configuration']['boost'] = 'asdf';
$this->assertConfigUpdateImport($name, $original_data, $custom_data);
}
/**
* Tests that a single set of plugin config stays in sync.
*
* @param \Drupal\Core\Entity\EntityWithPluginCollectionInterface $entity
* The entity.
* @param string $config_key
* Where the plugin config is stored.
* @param string $setting_key
* The setting within the plugin config to change.
* @param mixed $expected
* The expected default value of the plugin config setting.
*/
protected function checkSinglePluginConfigSync(EntityWithPluginCollectionInterface $entity, $config_key, $setting_key, $expected) {
$plugin_collection = $entity->getPluginCollections()[$config_key];
$settings = $entity->get($config_key);
// Ensure the default config exists.
$this->assertIdentical($expected, $settings[$setting_key]);
// Change the plugin config by setting it on the entity.
$settings[$setting_key] = $this->randomString();
$entity->set($config_key, $settings);
$entity->save();
$this->assertIdentical($settings, $entity->get($config_key));
$this->assertIdentical($settings, $plugin_collection->getConfiguration());
// Change the plugin config by setting it on the plugin.
$settings[$setting_key] = $this->randomString();
$plugin_collection->setConfiguration($settings);
$entity->save();
$this->assertIdentical($settings, $entity->get($config_key));
$this->assertIdentical($settings, $plugin_collection->getConfiguration());
}
/**
* Asserts that config entities are updated during import.
*
* @param string $name
* The name of the config object.
* @param array $original_data
* The original data stored in the config object.
* @param array $custom_data
* The new data to store in the config object.
*/
public function assertConfigUpdateImport($name, $original_data, $custom_data) {
$this->container->get('config.storage.staging')->write($name, $custom_data);
// Verify the active configuration still returns the default values.
$config = $this->config($name);
$this->assertIdentical($config->get(), $original_data);
// Import.
$this->configImporter()->import();
// Verify the values were updated.
$this->container->get('config.factory')->reset($name);
$config = $this->config($name);
$this->assertIdentical($config->get(), $custom_data);
}
}

View file

@ -0,0 +1,601 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\ConfigEntityQueryTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Config\Entity\Query\QueryFactory;
use Drupal\simpletest\KernelTestBase;
/**
* Tests Config Entity Query functionality.
*
* @group Entity
* @see \Drupal\Core\Config\Entity\Query
*/
class ConfigEntityQueryTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
static $modules = array('config_test');
/**
* Stores the search results for alter comparison.
*
* @var array
*/
protected $queryResults;
/**
* The query factory used to construct all queries in the test.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $factory;
/**
* Stores all config entities created for the test.
*
* @var array
*/
protected $entities;
protected function setUp() {
parent::setUp();
$this->entities = array();
$this->factory = $this->container->get('entity.query');
// These two are here to make sure that matchArray needs to go over several
// non-matches on every levels.
$array['level1']['level2a'] = 9;
$array['level1a']['level2'] = 9;
// The tests match array.level1.level2.
$array['level1']['level2'] = 1;
$entity = entity_create('config_query_test', array(
'label' => $this->randomMachineName(),
'id' => '1',
'number' => 31,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
$array['level1']['level2'] = 2;
$entity = entity_create('config_query_test', array(
'label' => $this->randomMachineName(),
'id' => '2',
'number' => 41,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
$array['level1']['level2'] = 1;
$entity = entity_create('config_query_test', array(
'label' => 'test_prefix_' . $this->randomMachineName(),
'id' => '3',
'number' => 59,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
$array['level1']['level2'] = 2;
$entity = entity_create('config_query_test', array(
'label' => $this->randomMachineName() . '_test_suffix',
'id' => '4',
'number' => 26,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
$array['level1']['level2'] = 3;
$entity = entity_create('config_query_test', array(
'label' => $this->randomMachineName() . '_TEST_contains_' . $this->randomMachineName(),
'id' => '5',
'number' => 53,
'array' => $array,
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
}
/**
* Tests basic functionality.
*/
public function testConfigEntityQuery() {
// Run a test without any condition.
$this->queryResults = $this->factory->get('config_query_test')
->execute();
$this->assertResults(array('1', '2', '3', '4', '5'));
// No conditions, OR.
$this->queryResults = $this->factory->get('config_query_test', 'OR')
->execute();
$this->assertResults(array('1', '2', '3', '4', '5'));
// Filter by ID with equality.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3')
->execute();
$this->assertResults(array('3'));
// Filter by label with a known prefix.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test_prefix', 'STARTS_WITH')
->execute();
$this->assertResults(array('3'));
// Filter by label with a known suffix.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test_suffix', 'ENDS_WITH')
->execute();
$this->assertResults(array('4'));
// Filter by label with a known containing word.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test_contains', 'CONTAINS')
->execute();
$this->assertResults(array('5'));
// Filter by ID with the IN operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', array('2', '3'), 'IN')
->execute();
$this->assertResults(array('2', '3'));
// Filter by ID with the implicit IN operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', array('2', '3'))
->execute();
$this->assertResults(array('2', '3'));
// Filter by ID with the > operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '>')
->execute();
$this->assertResults(array('4', '5'));
// Filter by ID with the >= operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '>=')
->execute();
$this->assertResults(array('3', '4', '5'));
// Filter by ID with the <> operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '<>')
->execute();
$this->assertResults(array('1', '2', '4', '5'));
// Filter by ID with the < operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '<')
->execute();
$this->assertResults(array('1', '2'));
// Filter by ID with the <= operator.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '<=')
->execute();
$this->assertResults(array('1', '2', '3'));
// Filter by two conditions on the same field.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test_pref', 'STARTS_WITH')
->condition('label', 'test_prefix', 'STARTS_WITH')
->execute();
$this->assertResults(array('3'));
// Filter by two conditions on different fields. The first query matches for
// a different ID, so the result is empty.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test_prefix', 'STARTS_WITH')
->condition('id', '5')
->execute();
$this->assertResults(array());
// Filter by two different conditions on different fields. This time the
// first condition matches on one item, but the second one does as well.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test_prefix', 'STARTS_WITH')
->condition('id', '3')
->execute();
$this->assertResults(array('3'));
// Filter by two different conditions, of which the first one matches for
// every entry, the second one as well, but just the third one filters so
// that just two are left.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '1', '>=')
->condition('number', 10, '>=')
->condition('number', 50, '>=')
->execute();
$this->assertResults(array('3', '5'));
// Filter with an OR condition group.
$this->queryResults = $this->factory->get('config_query_test', 'OR')
->condition('id', 1)
->condition('id', '2')
->execute();
$this->assertResults(array('1', '2'));
// Simplify it with IN.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', array('1', '2'))
->execute();
$this->assertResults(array('1', '2'));
// Try explicit IN.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', array('1', '2'), 'IN')
->execute();
$this->assertResults(array('1', '2'));
// Try not IN.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', array('1', '2'), 'NOT IN')
->execute();
$this->assertResults(array('3', '4', '5'));
// Filter with an OR condition group on different fields.
$this->queryResults = $this->factory->get('config_query_test', 'OR')
->condition('id', 1)
->condition('number', 41)
->execute();
$this->assertResults(array('1', '2'));
// Filter with an OR condition group on different fields but matching on the
// same entity.
$this->queryResults = $this->factory->get('config_query_test', 'OR')
->condition('id', 1)
->condition('number', 31)
->execute();
$this->assertResults(array('1'));
// NO simple conditions, YES complex conditions, 'AND'.
$query = $this->factory->get('config_query_test', 'AND');
$and_condition_1 = $query->orConditionGroup()
->condition('id', '2')
->condition('label', $this->entities[0]->label);
$and_condition_2 = $query->orConditionGroup()
->condition('id', 1)
->condition('label', $this->entities[3]->label);
$this->queryResults = $query
->condition($and_condition_1)
->condition($and_condition_2)
->execute();
$this->assertResults(array('1'));
// NO simple conditions, YES complex conditions, 'OR'.
$query = $this->factory->get('config_query_test', 'OR');
$and_condition_1 = $query->andConditionGroup()
->condition('id', 1)
->condition('label', $this->entities[0]->label);
$and_condition_2 = $query->andConditionGroup()
->condition('id', '2')
->condition('label', $this->entities[1]->label);
$this->queryResults = $query
->condition($and_condition_1)
->condition($and_condition_2)
->execute();
$this->assertResults(array('1', '2'));
// YES simple conditions, YES complex conditions, 'AND'.
$query = $this->factory->get('config_query_test', 'AND');
$and_condition_1 = $query->orConditionGroup()
->condition('id', '2')
->condition('label', $this->entities[0]->label);
$and_condition_2 = $query->orConditionGroup()
->condition('id', 1)
->condition('label', $this->entities[3]->label);
$this->queryResults = $query
->condition('number', 31)
->condition($and_condition_1)
->condition($and_condition_2)
->execute();
$this->assertResults(array('1'));
// YES simple conditions, YES complex conditions, 'OR'.
$query = $this->factory->get('config_query_test', 'OR');
$and_condition_1 = $query->orConditionGroup()
->condition('id', '2')
->condition('label', $this->entities[0]->label);
$and_condition_2 = $query->orConditionGroup()
->condition('id', 1)
->condition('label', $this->entities[3]->label);
$this->queryResults = $query
->condition('number', 53)
->condition($and_condition_1)
->condition($and_condition_2)
->execute();
$this->assertResults(array('1', '2', '4', '5'));
// Test the exists and notExists conditions.
$this->queryResults = $this->factory->get('config_query_test')
->exists('id')
->execute();
$this->assertResults(array('1', '2', '3', '4', '5'));
$this->queryResults = $this->factory->get('config_query_test')
->exists('non-existent')
->execute();
$this->assertResults(array());
$this->queryResults = $this->factory->get('config_query_test')
->notExists('id')
->execute();
$this->assertResults(array());
$this->queryResults = $this->factory->get('config_query_test')
->notExists('non-existent')
->execute();
$this->assertResults(array('1', '2', '3', '4', '5'));
}
/**
* Tests ID conditions.
*/
public function testStringIdConditions() {
// We need an entity with a non-numeric ID.
$entity = entity_create('config_query_test', array(
'label' => $this->randomMachineName(),
'id' => 'foo.bar',
));
$this->entities[] = $entity;
$entity->enforceIsNew();
$entity->save();
// Test 'STARTS_WITH' condition.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'foo.bar', 'STARTS_WITH')
->execute();
$this->assertResults(array('foo.bar'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'f', 'STARTS_WITH')
->execute();
$this->assertResults(array('foo.bar'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'miss', 'STARTS_WITH')
->execute();
$this->assertResults(array());
// Test 'CONTAINS' condition.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'foo.bar', 'CONTAINS')
->execute();
$this->assertResults(array('foo.bar'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'oo.ba', 'CONTAINS')
->execute();
$this->assertResults(array('foo.bar'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'miss', 'CONTAINS')
->execute();
$this->assertResults(array());
// Test 'ENDS_WITH' condition.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'foo.bar', 'ENDS_WITH')
->execute();
$this->assertResults(array('foo.bar'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'r', 'ENDS_WITH')
->execute();
$this->assertResults(array('foo.bar'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', 'miss', 'ENDS_WITH')
->execute();
$this->assertResults(array());
}
/**
* Tests count query.
*/
public function testCount() {
// Test count on no conditions.
$count = $this->factory->get('config_query_test')
->count()
->execute();
$this->assertIdentical($count, count($this->entities));
// Test count on a complex query.
$query = $this->factory->get('config_query_test', 'OR');
$and_condition_1 = $query->andConditionGroup()
->condition('id', 1)
->condition('label', $this->entities[0]->label);
$and_condition_2 = $query->andConditionGroup()
->condition('id', '2')
->condition('label', $this->entities[1]->label);
$count = $query
->condition($and_condition_1)
->condition($and_condition_2)
->count()
->execute();
$this->assertIdentical($count, 2);
}
/**
* Tests sorting and range on config entity queries.
*/
public function testSortRange() {
// Sort by simple ascending/descending.
$this->queryResults = $this->factory->get('config_query_test')
->sort('number', 'DESC')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('3', '5', '2', '1', '4'));
$this->queryResults = $this->factory->get('config_query_test')
->sort('number', 'ASC')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('4', '1', '2', '5', '3'));
// Apply some filters and sort.
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '>')
->sort('number', 'DESC')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('5', '4'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('id', '3', '>')
->sort('number', 'ASC')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('4', '5'));
// Apply a pager and sort.
$this->queryResults = $this->factory->get('config_query_test')
->sort('number', 'DESC')
->range('2', '2')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('2', '1'));
$this->queryResults = $this->factory->get('config_query_test')
->sort('number', 'ASC')
->range('2', '2')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('2', '5'));
// Add a range to a query without a start parameter.
$this->queryResults = $this->factory->get('config_query_test')
->range(0, '3')
->sort('id', 'ASC')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('1', '2', '3'));
// Apply a pager with limit 4.
$this->queryResults = $this->factory->get('config_query_test')
->pager('4', 0)
->sort('id', 'ASC')
->execute();
$this->assertIdentical(array_values($this->queryResults), array('1', '2', '3', '4'));
}
/**
* Tests dotted path matching.
*/
public function testDotted() {
$this->queryResults = $this->factory->get('config_query_test')
->condition('array.level1.*', 1)
->execute();
$this->assertResults(array('1', '3'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('*.level1.level2', 2)
->execute();
$this->assertResults(array('2', '4'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('array.level1.*', 3)
->execute();
$this->assertResults(array('5'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('array.level1.level2', 3)
->execute();
$this->assertResults(array('5'));
// Make sure that values on the wildcard level do not match if if there are
// sub-keys defined. This must not find anything even if entity 2 has a
// top-level key number with value 41.
$this->queryResults = $this->factory->get('config_query_test')
->condition('*.level1.level2', 41)
->execute();
$this->assertResults(array());
}
/**
* Tests case sensitivity.
*/
public function testCaseSensitivity() {
// Filter by label with a known containing case-sensitive word.
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'TEST', 'CONTAINS')
->execute();
$this->assertResults(array('3', '4', '5'));
$this->queryResults = $this->factory->get('config_query_test')
->condition('label', 'test', 'CONTAINS')
->execute();
$this->assertResults(array('3', '4', '5'));
}
/**
* Tests lookup keys are added to the key value store.
*/
public function testLookupKeys() {
\Drupal::service('state')->set('config_test.lookup_keys', TRUE);
\Drupal::entityManager()->clearCachedDefinitions();
$key_value = $this->container->get('keyvalue')->get(QueryFactory::CONFIG_LOOKUP_PREFIX . 'config_test');
$test_entities = [];
$entity = entity_create('config_test', array(
'label' => $this->randomMachineName(),
'id' => '1',
'style' => 'test',
));
$test_entities[$entity->getConfigDependencyName()] = $entity;
$entity->enforceIsNew();
$entity->save();
$expected[] = $entity->getConfigDependencyName();
$this->assertEqual($expected, $key_value->get('style:test'));
$entity = entity_create('config_test', array(
'label' => $this->randomMachineName(),
'id' => '2',
'style' => 'test',
));
$test_entities[$entity->getConfigDependencyName()] = $entity;
$entity->enforceIsNew();
$entity->save();
$expected[] = $entity->getConfigDependencyName();
$this->assertEqual($expected, $key_value->get('style:test'));
$entity = entity_create('config_test', array(
'label' => $this->randomMachineName(),
'id' => '3',
'style' => 'blah',
));
$entity->enforceIsNew();
$entity->save();
// Do not add this entity to the list of expected result as it has a
// different value.
$this->assertEqual($expected, $key_value->get('style:test'));
$this->assertEqual([$entity->getConfigDependencyName()], $key_value->get('style:blah'));
// Ensure that a delete clears a key.
$entity->delete();
$this->assertEqual([], $key_value->get('style:blah'));
// Ensure that delete only clears one key.
$entity_id = array_pop($expected);
$test_entities[$entity_id]->delete();
$this->assertEqual($expected, $key_value->get('style:test'));
$entity_id = array_pop($expected);
$test_entities[$entity_id]->delete();
$this->assertEqual($expected, $key_value->get('style:test'));
}
/**
* Asserts the results as expected regardless of order.
*
* @param array $expected
* Array of expected entity IDs.
*/
protected function assertResults($expected) {
$this->assertIdentical(count($this->queryResults), count($expected));
foreach ($expected as $value) {
// This also tests whether $this->queryResults[$value] is even set at all.
$this->assertIdentical($this->queryResults[$value], $value);
}
}
}

View file

@ -0,0 +1,545 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\ContentEntityChangedTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\entity_test\Entity\EntityTestMulChanged;
use Drupal\entity_test\Entity\EntityTestMulRevChanged;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests basic EntityChangedInterface functionality.
*
* @group Entity
*/
class ContentEntityChangedTest extends EntityUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['language', 'user', 'system', 'field', 'text', 'filter', 'entity_test'];
/**
* The EntityTestMulChanged entity type storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $mulChangedStorage;
/**
* The EntityTestMulRevChanged entity type storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $mulRevChangedStorage;
/**
* @inheritdoc
*/
protected function setUp() {
parent::setUp();
// Enable an additional language.
ConfigurableLanguage::createFromLangcode('de')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->installEntitySchema('entity_test_mul_changed');
$this->installEntitySchema('entity_test_mulrev_changed');
$this->mulChangedStorage = $this->entityManager->getStorage('entity_test_mul_changed');
$this->mulRevChangedStorage = $this->entityManager->getStorage('entity_test_mulrev_changed');
}
/**
* Tests basic EntityChangedInterface functionality.
*/
public function testChanged() {
$user1 = $this->createUser();
$user2 = $this->createUser();
// Create a test entity.
$entity = EntityTestMulChanged::create(array(
'name' => $this->randomString(),
'user_id' => $user1->id(),
'language' => 'en',
));
$entity->save();
$this->assertTrue(
$entity->getChangedTime() >= REQUEST_TIME,
'Changed time of original language is valid.'
);
// We can't assert equality here because the created time is set to the
// request time, while instances of ChangedTestItem use the current
// timestamp every time. Therefor we check if the changed timestamp is
// between the created time and now.
$this->assertTrue(
($entity->getChangedTime() >= $entity->get('created')->value) &&
(($entity->getChangedTime() - $entity->get('created')->value) <= time() - REQUEST_TIME),
'Changed and created time of original language can be assumed to be identical.'
);
$this->assertEqual(
$entity->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of original language is the same as changed time across all translations.'
);
$changed_en = $entity->getChangedTime();
/** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $german */
$german = $entity->addTranslation('de');
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertTrue(
$german->getChangedTime() > $entity->getChangedTime(),
'Changed time of the German translation is newer then the original language.'
);
$this->assertEqual(
$german->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of the German translation is the newest time across all translations.'
);
$changed_de = $german->getChangedTime();
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertEqual(
$german->getChangedTime(), $changed_de,
'Changed time of the German translation did not change.'
);
$entity->setOwner($user2);
$entity->save();
$this->assertTrue(
$entity->getChangedTime() > $changed_en,
'Changed time of original language did change.'
);
$this->assertEqual(
$german->getChangedTime(), $changed_de,
'Changed time of the German translation did not change.'
);
$this->assertTrue(
$entity->getChangedTime() > $german->getChangedTime(),
'Changed time of original language is newer then the German translation.'
);
$this->assertEqual(
$entity->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of the original language is the newest time across all translations.'
);
$changed_en = $entity->getChangedTime();
// Save entity without any changes.
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertEqual(
$german->getChangedTime(), $changed_de,
'Changed time of the German translation did not change.'
);
// At this point the changed time of the original language (en) is newer
// than the changed time of the German translation. Now test that entity
// queries work as expected.
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_en)->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time of original language.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_en, '=', 'en')->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time of original language by setting the original language as condition.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_de, '=', 'en')->execute();
$this->assertFalse(
$ids,
'There\'s no original entity stored having the changed time of the German translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_en)->condition('default_langcode', '1')->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time of default language.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_de)->condition('default_langcode', '1')->execute();
$this->assertFalse(
$ids,
'There\'s no entity stored using the default language having the changed time of the German translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_de)->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time of the German translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_de, '=', 'de')->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time of the German translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_en, '=', 'de')->execute();
$this->assertFalse(
$ids,
'There\'s no German translation stored having the changed time of the original language.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_de, '>')->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time regardless of translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_en, '<')->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time regardless of translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', 0, '>')->execute();
$this->assertEqual(
reset($ids), $entity->id(),
'Entity query can access changed time regardless of translation.'
);
$query = $this->mulChangedStorage->getQuery();
$ids = $query->condition('changed', $changed_en, '>')->execute();
$this->assertFalse(
$ids,
'Entity query can access changed time regardless of translation.'
);
}
/**
* Tests revisionable EntityChangedInterface functionality.
*/
public function testRevisionChanged() {
$user1 = $this->createUser();
$user2 = $this->createUser();
// Create a test entity.
$entity = EntityTestMulRevChanged::create(array(
'name' => $this->randomString(),
'user_id' => $user1->id(),
'language' => 'en',
));
$entity->save();
$this->assertTrue(
$entity->getChangedTime() >= REQUEST_TIME,
'Changed time of original language is valid.'
);
// We can't assert equality here because the created time is set to the
// request time while instances of ChangedTestItem use the current
// timestamp every time.
$this->assertTrue(
($entity->getChangedTime() >= $entity->get('created')->value) &&
(($entity->getChangedTime() - $entity->get('created')->value) <= time() - REQUEST_TIME),
'Changed and created time of original language can be assumed to be identical.'
);
$this->assertEqual(
$entity->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of original language is the same as changed time across all translations.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is set for a new entity.'
);
$changed_en = $entity->getChangedTime();
$entity->setNewRevision();
// Save entity without any changes but create new revision.
$entity->save();
// A new revision without any changes should not set a new changed time.
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is not set for new revision without changes.'
);
$entity->setNewRevision();
$entity->setOwner($user2);
$entity->save();
$this->assertTrue(
$entity->getChangedTime() > $changed_en,
'Changed time of original language has been updated by new revision.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is set for new revision with changes.'
);
$changed_en = $entity->getChangedTime();
/** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $german */
$german = $entity->addTranslation('de');
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertTrue(
$german->getChangedTime() > $entity->getChangedTime(),
'Changed time of the German translation is newer then the original language.'
);
$this->assertEqual(
$german->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of the German translation is the newest time across all translations.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is not reset by adding a new translation.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($german),
'Changed flag of German translation is set when adding the translation.'
);
$changed_de = $german->getChangedTime();
$entity->setNewRevision();
// Save entity without any changes but create new revision.
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertEqual(
$german->getChangedTime(), $changed_de,
'Changed time of the German translation did not change.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is not set for new revision without changes.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($german),
'Changed flag of of the German translation is not set for new revision without changes.'
);
$entity->setNewRevision();
$german->setOwner($user2);
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertTrue(
$german->getChangedTime() > $changed_de,
'Changed time of the German translation did change.'
);
$this->assertEqual(
$german->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of the German translation is the newest time across all translations.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is not set when changing the German Translation.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($german),
'Changed flag of German translation is set when changing the German translation.'
);
$french = $entity->getTranslation('fr');
$entity->setNewRevision();
$entity->save();
$this->assertEqual(
$entity->getChangedTime(), $changed_en,
'Changed time of original language did not change.'
);
$this->assertTrue(
$french->getChangedTime() > $entity->getChangedTime(),
'Changed time of the French translation is newer then the original language.'
);
$this->assertTrue(
$french->getChangedTime() > $entity->getChangedTime(),
'Changed time of the French translation is newer then the German translation.'
);
$this->assertEqual(
$french->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
'Changed time of the French translation is the newest time across all translations.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is reset by adding a new translation and a new revision.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($german),
'Changed flag of German translation is reset by adding a new translation and a new revision.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($french),
'Changed flag of French translation is set when adding the translation and a new revision.'
);
$entity->removeTranslation('fr');
$entity->setNewRevision();
$entity->save();
// This block simulates exactly the flow of a node form submission of a new
// translation and a new revision.
$form_entity_builder_entity = EntityTestMulRevChanged::load($entity->id());
// ContentTranslationController::prepareTranslation().
$form_entity_builder_entity = $form_entity_builder_entity->addTranslation('fr', $form_entity_builder_entity->toArray());
// EntityForm::buildEntity() during form submit.
$form_entity_builder_clone = clone $form_entity_builder_entity;
// NodeForm::submitForm().
$form_entity_builder_clone->setNewRevision();
// EntityForm::save().
$form_entity_builder_clone->save();
// The assertion fails unless https://www.drupal.org/node/2513094 is
// committed.
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($entity),
'Changed flag of original language is reset by adding a new translation and a new revision.'
);
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($german),
'Changed flag of German translation is reset by adding a new translation and a new revision.'
);
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($french),
'Changed flag of French translation is set when adding the translation and a new revision.'
);
$german->setOwner($user1);
$german->setRevisionTranslationAffected(FALSE);
$entity->save();
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($german),
'German translation changed but the changed flag is reset manually.'
);
$entity->setNewRevision();
$german->setRevisionTranslationAffected(TRUE);
$entity->save();
$this->assertTrue(
$this->getRevisionTranslationAffectedFlag($german),
'German translation is not changed and a new revision is created but the changed flag is set manually.'
);
$german->setOwner($user2);
$entity->setNewRevision();
$german->setRevisionTranslationAffected(FALSE);
$entity->save();
$this->assertFalse(
$this->getRevisionTranslationAffectedFlag($german),
'German translation changed and a new revision is created but the changed flag is reset manually.'
);
}
/**
* Retrieves the revision translation affected flag value.
*
* @param \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity
* The entity object to be checked.
*
* @return bool
* The flag value.
*/
protected function getRevisionTranslationAffectedFlag(EntityTestMulRevChanged $entity) {
$query = $this->mulRevChangedStorage->getQuery();
$ids = $query->condition('revision_translation_affected', 1, '=', $entity->language()->getId())->execute();
$id = reset($ids);
return (bool) ($id == $entity->id());
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\ContentEntityNullStorageTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\contact\Entity\ContactForm;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\simpletest\KernelTestBase;
/**
* Tests ContentEntityNullStorage entity query support.
*
* @see \Drupal\Core\Entity\ContentEntityNullStorage
* @see \Drupal\Core\Entity\Query\Null\Query
*
* @group Entity
*/
class ContentEntityNullStorageTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'contact', 'user');
/**
* Tests using entity query with ContentEntityNullStorage.
*
* @see \Drupal\Core\Entity\Query\Null\Query
*/
public function testEntityQuery() {
$this->assertIdentical(0, \Drupal::entityQuery('contact_message')->count()->execute(), 'Counting a null storage returns 0.');
$this->assertIdentical([], \Drupal::entityQuery('contact_message')->execute(), 'Querying a null storage returns an empty array.');
$this->assertIdentical([], \Drupal::entityQuery('contact_message')->condition('contact_form', 'test')->execute(), 'Querying a null storage returns an empty array and conditions are ignored.');
$this->assertIdentical([], \Drupal::entityQueryAggregate('contact_message')->aggregate('name', 'AVG')->execute(), 'Aggregate querying a null storage returns an empty array');
}
/**
* Tests deleting a contact form entity via a configuration import.
*
* @see \Drupal\Core\Entity\Event\BundleConfigImportValidate
*/
public function testDeleteThroughImport() {
$contact_form = ContactForm::create(['id' => 'test']);
$contact_form->save();
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.staging'),
$this->container->get('config.storage'),
$this->container->get('config.manager')
);
$config_importer = new ConfigImporter(
$storage_comparer->createChangelist(),
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('module_installer'),
$this->container->get('theme_handler'),
$this->container->get('string_translation')
);
// Delete the contact message in staging.
$staging = $this->container->get('config.storage.staging');
$staging->delete($contact_form->getConfigDependencyName());
// Import.
$config_importer->reset()->import();
$this->assertNull(ContactForm::load($contact_form->id()), 'The contact form has been deleted.');
}
}

View file

@ -0,0 +1,331 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\Element\EntityAutocompleteElementFormTest.
*/
namespace Drupal\system\Tests\Entity\Element;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
use Drupal\user\Entity\User;
/**
* Tests the EntityAutocomplete Form API element.
*
* @group Form
*/
class EntityAutocompleteElementFormTest extends EntityUnitTestBase implements FormInterface {
/**
* User for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $testUser;
/**
* User for autocreate testing.
*
* @var \Drupal\user\UserInterface
*/
protected $testAutocreateUser;
/**
* An array of entities to be referenced in this test.
*
* @var \Drupal\Core\Entity\EntityInterface[]
*/
protected $referencedEntities;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['router', 'key_value_expire']);
\Drupal::service('router.builder')->rebuild();
$this->testUser = User::create(array(
'name' => 'foobar1',
'mail' => 'foobar1@example.com',
));
$this->testUser->save();
\Drupal::service('current_user')->setAccount($this->testUser);
$this->testAutocreateUser = User::create(array(
'name' => 'foobar2',
'mail' => 'foobar2@example.com',
));
$this->testAutocreateUser->save();
for ($i = 1; $i < 3; $i++) {
$entity = EntityTest::create(array(
'name' => $this->randomMachineName()
));
$entity->save();
$this->referencedEntities[] = $entity;
}
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'test_entity_autocomplete';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['single'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
);
$form['single_autocreate'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#autocreate' => array(
'bundle' => 'entity_test',
),
);
$form['single_autocreate_specific_uid'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#autocreate' => array(
'bundle' => 'entity_test',
'uid' => $this->testAutocreateUser->id(),
),
);
$form['tags'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#tags' => TRUE,
);
$form['tags_autocreate'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#tags' => TRUE,
'#autocreate' => array(
'bundle' => 'entity_test',
),
);
$form['tags_autocreate_specific_uid'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#tags' => TRUE,
'#autocreate' => array(
'bundle' => 'entity_test',
'uid' => $this->testAutocreateUser->id(),
),
);
$form['single_no_validate'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#validate_reference' => FALSE,
);
$form['single_autocreate_no_validate'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#autocreate' => array(
'bundle' => 'entity_test',
),
);
$form['single_access'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#default_value' => $this->referencedEntities[0],
);
$form['tags_access'] = array(
'#type' => 'entity_autocomplete',
'#target_type' => 'entity_test',
'#tags' => TRUE,
'#default_value' => array($this->referencedEntities[0], $this->referencedEntities[1]),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) { }
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) { }
/**
* Tests valid entries in the EntityAutocomplete Form API element.
*/
public function testValidEntityAutocompleteElement() {
$form_state = (new FormState())
->setValues([
'single' => $this->getAutocompleteInput($this->referencedEntities[0]),
'single_autocreate' => 'single - autocreated entity label',
'single_autocreate_specific_uid' => 'single - autocreated entity label with specific uid',
'tags' => $this->getAutocompleteInput($this->referencedEntities[0]) . ', ' . $this->getAutocompleteInput($this->referencedEntities[1]),
'tags_autocreate' =>
$this->getAutocompleteInput($this->referencedEntities[0])
. ', tags - autocreated entity label, '
. $this->getAutocompleteInput($this->referencedEntities[1]),
'tags_autocreate_specific_uid' =>
$this->getAutocompleteInput($this->referencedEntities[0])
. ', tags - autocreated entity label with specific uid, '
. $this->getAutocompleteInput($this->referencedEntities[1]),
]);
$form_builder = $this->container->get('form_builder');
$form_builder->submitForm($this, $form_state);
// Valid form state.
$this->assertEqual(count($form_state->getErrors()), 0);
// Test the 'single' element.
$this->assertEqual($form_state->getValue('single'), $this->referencedEntities[0]->id());
// Test the 'single_autocreate' element.
$value = $form_state->getValue('single_autocreate');
$this->assertEqual($value['entity']->label(), 'single - autocreated entity label');
$this->assertEqual($value['entity']->bundle(), 'entity_test');
$this->assertEqual($value['entity']->getOwnerId(), $this->testUser->id());
// Test the 'single_autocreate_specific_uid' element.
$value = $form_state->getValue('single_autocreate_specific_uid');
$this->assertEqual($value['entity']->label(), 'single - autocreated entity label with specific uid');
$this->assertEqual($value['entity']->bundle(), 'entity_test');
$this->assertEqual($value['entity']->getOwnerId(), $this->testAutocreateUser->id());
// Test the 'tags' element.
$expected = array(
array('target_id' => $this->referencedEntities[0]->id()),
array('target_id' => $this->referencedEntities[1]->id()),
);
$this->assertEqual($form_state->getValue('tags'), $expected);
// Test the 'single_autocreate' element.
$value = $form_state->getValue('tags_autocreate');
// First value is an existing entity.
$this->assertEqual($value[0]['target_id'], $this->referencedEntities[0]->id());
// Second value is an autocreated entity.
$this->assertTrue(!isset($value[1]['target_id']));
$this->assertEqual($value[1]['entity']->label(), 'tags - autocreated entity label');
$this->assertEqual($value[1]['entity']->getOwnerId(), $this->testUser->id());
// Third value is an existing entity.
$this->assertEqual($value[2]['target_id'], $this->referencedEntities[1]->id());
// Test the 'tags_autocreate_specific_uid' element.
$value = $form_state->getValue('tags_autocreate_specific_uid');
// First value is an existing entity.
$this->assertEqual($value[0]['target_id'], $this->referencedEntities[0]->id());
// Second value is an autocreated entity.
$this->assertTrue(!isset($value[1]['target_id']));
$this->assertEqual($value[1]['entity']->label(), 'tags - autocreated entity label with specific uid');
$this->assertEqual($value[1]['entity']->getOwnerId(), $this->testAutocreateUser->id());
// Third value is an existing entity.
$this->assertEqual($value[2]['target_id'], $this->referencedEntities[1]->id());
}
/**
* Tests invalid entries in the EntityAutocomplete Form API element.
*/
public function testInvalidEntityAutocompleteElement() {
$form_builder = $this->container->get('form_builder');
// Test 'single' with a entity label that doesn't exist
$form_state = (new FormState())
->setValues([
'single' => 'single - non-existent label',
]);
$form_builder->submitForm($this, $form_state);
$this->assertEqual(count($form_state->getErrors()), 1);
$this->assertEqual($form_state->getErrors()['single'], t('There are no entities matching "%value".', array('%value' => 'single - non-existent label')));
// Test 'single' with a entity ID that doesn't exist.
$form_state = (new FormState())
->setValues([
'single' => 'single - non-existent label (42)',
]);
$form_builder->submitForm($this, $form_state);
$this->assertEqual(count($form_state->getErrors()), 1);
$this->assertEqual($form_state->getErrors()['single'], t('The referenced entity (%type: %id) does not exist.', array('%type' => 'entity_test', '%id' => 42)));
// Do the same tests as above but on an element with '#validate_reference'
// set to FALSE.
$form_state = (new FormState())
->setValues([
'single_no_validate' => 'single - non-existent label',
'single_autocreate_no_validate' => 'single - autocreate non-existent label'
]);
$form_builder->submitForm($this, $form_state);
// The element without 'autocreate' support still has to emit a warning when
// the input doesn't end with an entity ID enclosed in parentheses.
$this->assertEqual(count($form_state->getErrors()), 1);
$this->assertEqual($form_state->getErrors()['single_no_validate'], t('There are no entities matching "%value".', array('%value' => 'single - non-existent label')));
$form_state = (new FormState())
->setValues([
'single_no_validate' => 'single - non-existent label (42)',
'single_autocreate_no_validate' => 'single - autocreate non-existent label (43)'
]);
$form_builder->submitForm($this, $form_state);
// The input is complete (i.e. contains an entity ID at the end), no errors
// are triggered.
$this->assertEqual(count($form_state->getErrors()), 0);
}
/**
* Tests that access is properly checked by the EntityAutocomplete element.
*/
public function testEntityAutocompleteAccess() {
$form_builder = $this->container->get('form_builder');
$form = $form_builder->getForm($this);
// Check that the current user has proper access to view entity labels.
$expected = $this->referencedEntities[0]->label() . ' (' . $this->referencedEntities[0]->id() . ')';
$this->assertEqual($form['single_access']['#value'], $expected);
$expected .= ', ' . $this->referencedEntities[1]->label() . ' (' . $this->referencedEntities[1]->id() . ')';
$this->assertEqual($form['tags_access']['#value'], $expected);
// Set up a non-admin user that is *not* allowed to view test entities.
\Drupal::currentUser()->setAccount($this->createUser(array(), array()));
// Rebuild the form.
$form = $form_builder->getForm($this);
$expected = t('- Restricted access -') . ' (' . $this->referencedEntities[0]->id() . ')';
$this->assertEqual($form['single_access']['#value'], $expected);
$expected .= ', ' . t('- Restricted access -') . ' (' . $this->referencedEntities[1]->id() . ')';
$this->assertEqual($form['tags_access']['#value'], $expected);
}
/**
* Returns an entity label in the format needed by the EntityAutocomplete
* element.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A Drupal entity.
*
* @return string
* A string that can be used as a value for EntityAutocomplete elements.
*/
protected function getAutocompleteInput(EntityInterface $entity) {
return EntityAutocomplete::getEntityLabels(array($entity));
}
}

View file

@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityAccessControlHandlerTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the entity access control handler.
*
* @group Entity
*/
class EntityAccessControlHandlerTest extends EntityLanguageTestBase {
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'url_alias');
}
/**
* Asserts entity access correctly grants or denies access.
*/
function assertEntityAccess($ops, AccessibleInterface $object, AccountInterface $account = NULL) {
foreach ($ops as $op => $result) {
$message = format_string("Entity access returns @result with operation '@op'.", array(
'@result' => !isset($result) ? 'null' : ($result ? 'true' : 'false'),
'@op' => $op,
));
$this->assertEqual($result, $object->access($op, $account), $message);
}
}
/**
* Ensures entity access is properly working.
*/
function testEntityAccess() {
// Set up a non-admin user that is allowed to view test entities.
\Drupal::currentUser()->setAccount($this->createUser(array('uid' => 2), array('view test entity')));
$entity = entity_create('entity_test', array(
'name' => 'test',
));
// The current user is allowed to view entities.
$this->assertEntityAccess(array(
'create' => FALSE,
'update' => FALSE,
'delete' => FALSE,
'view' => TRUE,
), $entity);
// The custom user is not allowed to perform any operation on test entities.
$custom_user = $this->createUser();
$this->assertEntityAccess(array(
'create' => FALSE,
'update' => FALSE,
'delete' => FALSE,
'view' => FALSE,
), $entity, $custom_user);
}
/**
* Ensures default entity access is checked when necessary.
*
* This ensures that the default checkAccess() implementation of the
* entity access control handler is considered if hook_entity_access() has not
* explicitly forbidden access. Therefore the default checkAccess()
* implementation can forbid access, even after access was already explicitly
* allowed by hook_entity_access().
*
* @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
* @see entity_test_entity_access()
*/
function testDefaultEntityAccess() {
// Set up a non-admin user that is allowed to view test entities.
\Drupal::currentUser()->setAccount($this->createUser(array('uid' => 2), array('view test entity')));
$entity = entity_create('entity_test', array(
'name' => 'forbid_access',
));
// The user is denied access to the entity.
$this->assertEntityAccess(array(
'create' => FALSE,
'update' => FALSE,
'delete' => FALSE,
'view' => FALSE,
), $entity);
}
/**
* Ensures that the default handler is used as a fallback.
*/
function testEntityAccessDefaultController() {
// The implementation requires that the global user id can be loaded.
\Drupal::currentUser()->setAccount($this->createUser(array('uid' => 2)));
// Check that the default access control handler is used for entities that don't
// have a specific access control handler defined.
$handler = $this->container->get('entity.manager')->getAccessControlHandler('entity_test_default_access');
$this->assertTrue($handler instanceof EntityAccessControlHandler, 'The default entity handler is used for the entity_test_default_access entity type.');
$entity = entity_create('entity_test_default_access');
$this->assertEntityAccess(array(
'create' => FALSE,
'update' => FALSE,
'delete' => FALSE,
'view' => FALSE,
), $entity);
}
/**
* Ensures entity access for entity translations is properly working.
*/
function testEntityTranslationAccess() {
// Set up a non-admin user that is allowed to view test entity translations.
\Drupal::currentUser()->setAccount($this->createUser(array('uid' => 2), array('view test entity translations')));
// Create two test languages.
foreach (array('foo', 'bar') as $langcode) {
ConfigurableLanguage::create(array(
'id' => $langcode,
'label' => $this->randomString(),
))->save();
}
$entity = entity_create('entity_test', array(
'name' => 'test',
'langcode' => 'foo',
));
$entity->save();
$translation = $entity->getTranslation('bar');
$this->assertEntityAccess(array(
'view' => TRUE,
), $translation);
}
/**
* Tests hook invocations.
*/
public function testHooks() {
$state = $this->container->get('state');
$entity = entity_create('entity_test', array(
'name' => 'test',
));
// Test hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
$entity->access('create');
$this->assertEqual($state->get('entity_test_entity_create_access'), TRUE);
$this->assertEqual($state->get('entity_test_entity_test_create_access'), TRUE);
// Test hook_entity_access() and hook_ENTITY_TYPE_access().
$entity->access('view');
$this->assertEqual($state->get('entity_test_entity_access'), TRUE);
$this->assertEqual($state->get('entity_test_entity_test_access'), TRUE);
}
}

View file

@ -0,0 +1,146 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityApiTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\user\UserInterface;
/**
* Tests basic CRUD functionality.
*
* @group Entity
*/
class EntityApiTest extends EntityUnitTestBase {
/**
* @inheritdoc
*/
protected function setUp() {
parent::setUp();
foreach (entity_test_entity_types() as $entity_type_id) {
// The entity_test schema is installed by the parent.
if ($entity_type_id != 'entity_test') {
$this->installEntitySchema($entity_type_id);
}
}
}
/**
* Tests basic CRUD functionality of the Entity API.
*/
public function testCRUD() {
// All entity variations have to have the same results.
foreach (entity_test_entity_types() as $entity_type) {
$this->assertCRUD($entity_type, $this->createUser());
}
}
/**
* Executes a test set for a defined entity type and user.
*
* @param string $entity_type
* The entity type to run the tests with.
* @param \Drupal\user\UserInterface $user1
* The user to run the tests with.
*/
protected function assertCRUD($entity_type, UserInterface $user1) {
// Create some test entities.
$entity = entity_create($entity_type, array('name' => 'test', 'user_id' => $user1->id()));
$entity->save();
$entity = entity_create($entity_type, array('name' => 'test2', 'user_id' => $user1->id()));
$entity->save();
$entity = entity_create($entity_type, array('name' => 'test', 'user_id' => NULL));
$entity->save();
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test')));
$this->assertEqual($entities[0]->name->value, 'test', format_string('%entity_type: Created and loaded entity', array('%entity_type' => $entity_type)));
$this->assertEqual($entities[1]->name->value, 'test', format_string('%entity_type: Created and loaded entity', array('%entity_type' => $entity_type)));
// Test loading a single entity.
$loaded_entity = entity_load($entity_type, $entity->id());
$this->assertEqual($loaded_entity->id(), $entity->id(), format_string('%entity_type: Loaded a single entity by id.', array('%entity_type' => $entity_type)));
// Test deleting an entity.
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test2')));
$entities[0]->delete();
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test2')));
$this->assertEqual($entities, array(), format_string('%entity_type: Entity deleted.', array('%entity_type' => $entity_type)));
// Test updating an entity.
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test')));
$entities[0]->name->value = 'test3';
$entities[0]->save();
$entity = entity_load($entity_type, $entities[0]->id());
$this->assertEqual($entity->name->value, 'test3', format_string('%entity_type: Entity updated.', array('%entity_type' => $entity_type)));
// Try deleting multiple test entities by deleting all.
$ids = array_keys(entity_load_multiple($entity_type));
entity_delete_multiple($entity_type, $ids);
$all = entity_load_multiple($entity_type);
$this->assertTrue(empty($all), format_string('%entity_type: Deleted all entities.', array('%entity_type' => $entity_type)));
// Verify that all data got deleted.
$definition = \Drupal::entityManager()->getDefinition($entity_type);
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {' . $definition->getBaseTable() . '}')->fetchField(), 'Base table was emptied');
if ($data_table = $definition->getDataTable()) {
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {' . $data_table . '}')->fetchField(), 'Data table was emptied');
}
if ($revision_table = $definition->getRevisionTable()) {
$this->assertEqual(0, db_query('SELECT COUNT(*) FROM {' . $revision_table . '}')->fetchField(), 'Data table was emptied');
}
}
/**
* Tests that exceptions are thrown when saving or deleting an entity.
*/
public function testEntityStorageExceptionHandling() {
$entity = entity_create('entity_test', array('name' => 'test'));
try {
$GLOBALS['entity_test_throw_exception'] = TRUE;
$entity->save();
$this->fail('Entity presave EntityStorageException thrown but not caught.');
}
catch (EntityStorageException $e) {
$this->assertEqual($e->getcode(), 1, 'Entity presave EntityStorageException caught.');
}
$entity = entity_create('entity_test', array('name' => 'test2'));
try {
unset($GLOBALS['entity_test_throw_exception']);
$entity->save();
$this->pass('Exception presave not thrown and not caught.');
}
catch (EntityStorageException $e) {
$this->assertNotEqual($e->getCode(), 1, 'Entity presave EntityStorageException caught.');
}
$entity = entity_create('entity_test', array('name' => 'test3'));
$entity->save();
try {
$GLOBALS['entity_test_throw_exception'] = TRUE;
$entity->delete();
$this->fail('Entity predelete EntityStorageException not thrown.');
}
catch (EntityStorageException $e) {
$this->assertEqual($e->getCode(), 2, 'Entity predelete EntityStorageException caught.');
}
unset($GLOBALS['entity_test_throw_exception']);
$entity = entity_create('entity_test', array('name' => 'test4'));
$entity->save();
try {
$entity->delete();
$this->pass('Entity predelete EntityStorageException not thrown and not caught.');
}
catch (EntityStorageException $e) {
$this->assertNotEqual($e->getCode(), 2, 'Entity predelete EntityStorageException thrown.');
}
}
}

View file

@ -0,0 +1,164 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityAutocompleteTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Site\Settings;
use Drupal\system\Controller\EntityAutocompleteController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Tests the autocomplete functionality.
*
* @group Entity
*/
class EntityAutocompleteTest extends EntityUnitTestBase {
/**
* The entity type used in this test.
*
* @var string
*/
protected $entityType = 'entity_test';
/**
* The bundle used in this test.
*
* @var string
*/
protected $bundle = 'entity_test';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['key_value']);
}
/**
* Tests autocompletion edge cases with slashes in the names.
*/
function testEntityReferenceAutocompletion() {
// Add an entity with a slash in its name.
$entity_1 = entity_create($this->entityType, array('name' => '10/16/2011'));
$entity_1->save();
// Add another entity that differs after the slash character.
$entity_2 = entity_create($this->entityType, array('name' => '10/17/2011'));
$entity_2->save();
// Add another entity that has both a comma and a slash character.
$entity_3 = entity_create($this->entityType, array('name' => 'label with, and / test'));
$entity_3->save();
// Try to autocomplete a entity label that matches both entities.
// We should get both entities in a JSON encoded string.
$input = '10/';
$data = $this->getAutocompleteResult($input);
$this->assertIdentical($data[0]['label'], SafeMarkup::checkPlain($entity_1->name->value), 'Autocomplete returned the first matching entity');
$this->assertIdentical($data[1]['label'], SafeMarkup::checkPlain($entity_2->name->value), 'Autocomplete returned the second matching entity');
// Try to autocomplete a entity label that matches the first entity.
// We should only get the first entity in a JSON encoded string.
$input = '10/16';
$data = $this->getAutocompleteResult($input);
$target = array(
'value' => $entity_1->name->value . ' (1)',
'label' => SafeMarkup::checkPlain($entity_1->name->value),
);
$this->assertIdentical(reset($data), $target, 'Autocomplete returns only the expected matching entity.');
// Try to autocomplete a entity label that matches the second entity, and
// the first entity is already typed in the autocomplete (tags) widget.
$input = $entity_1->name->value . ' (1), 10/17';
$data = $this->getAutocompleteResult($input);
$this->assertIdentical($data[0]['label'], SafeMarkup::checkPlain($entity_2->name->value), 'Autocomplete returned the second matching entity');
// Try to autocomplete a entity label with both a comma and a slash.
$input = '"label with, and / t';
$data = $this->getAutocompleteResult($input);
$n = $entity_3->name->value . ' (3)';
// Entity labels containing commas or quotes must be wrapped in quotes.
$n = Tags::encode($n);
$target = array(
'value' => $n,
'label' => SafeMarkup::checkPlain($entity_3->name->value),
);
$this->assertIdentical(reset($data), $target, 'Autocomplete returns an entity label containing a comma and a slash.');
}
/**
* Tests that missing or invalid selection setting key are handled correctly.
*/
public function testSelectionSettingsHandling() {
$entity_reference_controller = EntityAutocompleteController::create($this->container);
$request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default');
$request->query->set('q', $this->randomString());
try {
// Pass an invalid selection settings key (i.e. one that does not exist
// in the key/value store).
$selection_settings_key = $this->randomString();
$entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key);
$this->fail('Non-existent selection settings key throws an exception.');
}
catch (AccessDeniedHttpException $e) {
$this->pass('Non-existent selection settings key throws an exception.');
}
try {
// Generate a valid hash key but store a modified settings array.
$selection_settings = [];
$selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt());
$selection_settings[$this->randomMachineName()] = $this->randomString();
\Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings);
$entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key);
}
catch (AccessDeniedHttpException $e) {
if ($e->getMessage() == 'Invalid selection settings key.') {
$this->pass('Invalid selection settings key throws an exception.');
}
else {
$this->fail('Invalid selection settings key throws an exception.');
}
}
}
/**
* Returns the result of an Entity reference autocomplete request.
*
* @param string $input
* The label of the entity to query by.
*
* @return mixed
* The JSON value encoded in its appropriate PHP type.
*/
protected function getAutocompleteResult($input) {
$request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default');
$request->query->set('q', $input);
$selection_settings = [];
$selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt());
\Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings);
$entity_reference_controller = EntityAutocompleteController::create($this->container);
$result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key)->getContent();
return Json::decode($result);
}
}

View file

@ -0,0 +1,119 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityBundleFieldTest.
*/
namespace Drupal\system\Tests\Entity;
/**
* Tests adding a custom bundle field.
*
* @group Entity
*/
class EntityBundleFieldTest extends EntityUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_schema_test');
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The database connection used.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('user', array('users_data'));
$this->installSchema('system', array('router'));
$this->moduleHandler = $this->container->get('module_handler');
$this->database = $this->container->get('database');
}
/**
* Tests making use of a custom bundle field.
*/
public function testCustomBundleFieldUsage() {
entity_test_create_bundle('custom');
// Check that an entity with bundle entity_test does not have the custom
// field.
$storage = $this->entityManager->getStorage('entity_test');
$entity = $storage->create([
'type' => 'entity_test',
]);
$this->assertFalse($entity->hasField('custom_bundle_field'));
// Check that the custom bundle has the defined custom field and check
// saving and deleting of custom field data.
$entity = $storage->create([
'type' => 'custom',
]);
$this->assertTrue($entity->hasField('custom_bundle_field'));
// Ensure that the field exists in the field map.
$field_map = \Drupal::entityManager()->getFieldMap();
$this->assertEqual($field_map['entity_test']['custom_bundle_field'], ['type' => 'string', 'bundles' => ['custom' => 'custom']]);
$entity->custom_bundle_field->value = 'swanky';
$entity->save();
$storage->resetCache();
$entity = $storage->load($entity->id());
$this->assertEqual($entity->custom_bundle_field->value, 'swanky', 'Entity was saved correctly');
$entity->custom_bundle_field->value = 'cozy';
$entity->save();
$storage->resetCache();
$entity = $storage->load($entity->id());
$this->assertEqual($entity->custom_bundle_field->value, 'cozy', 'Entity was updated correctly.');
$entity->delete();
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $storage->getTableMapping();
$table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'));
$result = $this->database->select($table, 'f')
->fields('f')
->condition('f.entity_id', $entity->id())
->execute();
$this->assertFalse($result->fetchAssoc(), 'Field data has been deleted');
// Create another entity to test that values are marked as deleted when a
// bundle is deleted.
$entity = $storage->create(['type' => 'custom', 'custom_bundle_field' => 'new']);
$entity->save();
entity_test_delete_bundle('custom');
$table = $table_mapping->getDedicatedDataTableName($entity->getFieldDefinition('custom_bundle_field'));
$result = $this->database->select($table, 'f')
->condition('f.entity_id', $entity->id())
->condition('deleted', 1)
->countQuery()
->execute();
$this->assertEqual(1, $result->fetchField(), 'Field data has been deleted');
// Ensure that the field no longer exists in the field map.
$field_map = \Drupal::entityManager()->getFieldMap();
$this->assertFalse(isset($field_map['entity_test']['custom_bundle_field']));
// @todo Test field purge and table deletion once supported. See
// https://www.drupal.org/node/2282119.
// $this->assertFalse($this->database->schema()->tableExists($table), 'Custom field table was deleted');
}
}

View file

@ -0,0 +1,706 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityCacheTagsTestBase.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\system\Tests\Cache\PageCacheTagsTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Provides helper methods for Entity cache tags tests.
*/
abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_reference', 'entity_test', 'field_test');
/**
* The main entity used for testing.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* The entity instance referencing the main entity.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $referencingEntity;
/**
* The entity instance not referencing the main entity.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $nonReferencingEntity;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Give anonymous users permission to view test entities, so that we can
// verify the cache tags of cached versions of test entity pages.
$user_role = Role::load(RoleInterface::ANONYMOUS_ID);
$user_role->grantPermission('view test entity');
$user_role->save();
// Create an entity.
$this->entity = $this->createEntity();
// If this is an entity with field UI enabled, then add a configurable
// field. We will use this configurable field in later tests to ensure that
// field configuration invalidate render cache entries.
if ($this->entity->getEntityType()->get('field_ui_base_route')) {
// Add field, so we can modify the field storage and field entities to
// verify that changes to those indeed clear cache tags.
entity_create('field_storage_config', array(
'field_name' => 'configurable_field',
'entity_type' => $this->entity->getEntityTypeId(),
'type' => 'test_field',
'settings' => array(),
))->save();
entity_create('field_config', array(
'entity_type' => $this->entity->getEntityTypeId(),
'bundle' => $this->entity->bundle(),
'field_name' => 'configurable_field',
'label' => 'Configurable field',
'settings' => array(),
))->save();
// Reload the entity now that a new field has been added to it.
$storage = $this->container
->get('entity.manager')
->getStorage($this->entity->getEntityTypeId());
$storage->resetCache();
$this->entity = $storage->load($this->entity->id());
}
// Create a referencing and a non-referencing entity.
list(
$this->referencingEntity,
$this->nonReferencingEntity,
) = $this->createReferenceTestEntities($this->entity);
}
/**
* Generates standardized entity cache tags test info.
*
* @param string $entity_type_label
* The label of the entity type whose cache tags to test.
* @param string $group
* The test group.
*
* @return array
*
* @see \Drupal\simpletest\TestBase::getInfo()
*/
protected static function generateStandardizedInfo($entity_type_label, $group) {
return array(
'name' => "$entity_type_label entity cache tags",
'description' => "Test the $entity_type_label entity's cache tags.",
'group' => $group,
);
}
/**
* Creates the entity to be tested.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity to be tested.
*/
abstract protected function createEntity();
/**
* Returns the access cache contexts for the tested entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be tested, as created by createEntity().
*
* @return string[]
* An array of the additional cache contexts.
*
* @see \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected function getAccessCacheContextsForEntity(EntityInterface $entity) {
return ['user.permissions'];
}
/**
* Returns the additional (non-standard) cache contexts for the tested entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be tested, as created by createEntity().
*
* @return string[]
* An array of the additional cache contexts.
*
* @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity()
*/
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
return [];
}
/**
* Returns the additional (non-standard) cache tags for the tested entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be tested, as created by createEntity().
* @return array
* An array of the additional cache tags.
*
* @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity()
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) {
return array();
}
/**
* Returns the additional cache tags for the tested entity's listing by type.
*
* @return string[]
* An array of the additional cache contexts.
*/
protected function getAdditionalCacheContextsForEntityListing() {
return [];
}
/**
* Returns the additional cache tags for the tested entity's listing by type.
*
* Necessary when there are unavoidable default entities of this type, e.g.
* the anonymous and administrator User entities always exist.
*
* @return array
* An array of the additional cache tags.
*/
protected function getAdditionalCacheTagsForEntityListing() {
return [];
}
/**
* Selects the preferred view mode for the given entity type.
*
* Prefers 'full', picks the first one otherwise, and if none are available,
* chooses 'default'.
*/
protected function selectViewMode($entity_type) {
$view_modes = \Drupal::entityManager()
->getStorage('entity_view_mode')
->loadByProperties(array('targetEntityType' => $entity_type));
if (empty($view_modes)) {
return 'default';
}
else {
// Prefer the "full" display mode.
if (isset($view_modes[$entity_type . '.full'])) {
return 'full';
}
else {
$view_modes = array_keys($view_modes);
return substr($view_modes[0], strlen($entity_type) + 1);
}
}
}
/**
* Creates a referencing and a non-referencing entity for testing purposes.
*
* @param \Drupal\Core\Entity\EntityInterface $referenced_entity
* The entity that the referencing entity should reference.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* An array containing a referencing entity and a non-referencing entity.
*/
protected function createReferenceTestEntities($referenced_entity) {
// All referencing entities should be of the type 'entity_test'.
$entity_type = 'entity_test';
// Create a "foo" bundle for the given entity type.
$bundle = 'foo';
entity_test_create_bundle($bundle, NULL, $entity_type);
// Add a field of the given type to the given entity type's "foo" bundle.
$field_name = $referenced_entity->getEntityTypeId() . '_reference';
entity_create('field_storage_config', array(
'field_name' => $field_name,
'entity_type' => $entity_type,
'type' => 'entity_reference',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'settings' => array(
'target_type' => $referenced_entity->getEntityTypeId(),
),
))->save();
entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => $entity_type,
'bundle' => $bundle,
'settings' => array(
'handler' => 'default',
'handler_settings' => array(
'target_bundles' => array(
$referenced_entity->bundle() => $referenced_entity->bundle(),
),
'sort' => array('field' => '_none'),
'auto_create' => FALSE,
),
),
))->save();
if (!$this->entity->getEntityType()->hasHandlerClass('view_builder')) {
entity_get_display($entity_type, $bundle, 'full')
->setComponent($field_name, array(
'type' => 'entity_reference_label',
))
->save();
}
else {
$referenced_entity_view_mode = $this->selectViewMode($this->entity->getEntityTypeId());
entity_get_display($entity_type, $bundle, 'full')
->setComponent($field_name, array(
'type' => 'entity_reference_entity_view',
'settings' => array(
'view_mode' => $referenced_entity_view_mode,
),
))
->save();
}
// Create an entity that does reference the entity being tested.
$label_key = \Drupal::entityManager()->getDefinition($entity_type)->getKey('label');
$referencing_entity = entity_create($entity_type, array(
$label_key => 'Referencing ' . $entity_type,
'status' => 1,
'type' => $bundle,
$field_name => array('target_id' => $referenced_entity->id()),
));
$referencing_entity->save();
// Create an entity that does not reference the entity being tested.
$non_referencing_entity = entity_create($entity_type, array(
$label_key => 'Non-referencing ' . $entity_type,
'status' => 1,
'type' => $bundle,
));
$non_referencing_entity->save();
return array(
$referencing_entity,
$non_referencing_entity,
);
}
/**
* Tests cache tags presence and invalidation of the entity when referenced.
*
* Tests the following cache tags:
* - entity type view cache tag: "<entity type>_view"
* - entity cache tag: "<entity type>:<entity ID>"
* - entity type list cache tag: "<entity type>_list"
* - referencing entity type view cache tag: "<referencing entity type>_view"
* - referencing entity type cache tag: "<referencing entity type>:<referencing entity ID>"
*/
public function testReferencedEntity() {
$entity_type = $this->entity->getEntityTypeId();
$referencing_entity_url = $this->referencingEntity->urlInfo('canonical');
$non_referencing_entity_url = $this->nonReferencingEntity->urlInfo('canonical');
$listing_url = Url::fromRoute('entity.entity_test.collection_referencing_entities', [
'entity_reference_field_name' => $entity_type . '_reference',
'referenced_entity_type' => $entity_type,
'referenced_entity_id' => $this->entity->id(),
]);
$empty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_empty', ['entity_type_id' => $entity_type]);
$nonempty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_labels_alphabetically', ['entity_type_id' => $entity_type]);
// The default cache contexts for rendered entities.
$default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme'];
$entity_cache_contexts = $default_cache_contexts;
// Cache tags present on every rendered page.
$page_cache_tags = Cache::mergeTags(
['rendered'],
// If the block module is used, the Block page display variant is used,
// which adds the block config entity type's list cache tags.
\Drupal::moduleHandler()->moduleExists('block') ? ['config:block_list']: []
);
$page_cache_tags_referencing_entity = in_array('user.permissions', $this->getAccessCacheContextsForEntity($this->referencingEntity)) ? ['config:user.role.anonymous'] : [];
$view_cache_tag = array();
if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
$view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)
->getCacheTags();
}
// Generate the cache tags for the (non) referencing entities.
$referencing_entity_cache_tags = Cache::mergeTags(
$this->referencingEntity->getCacheTags(),
\Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags(),
// Includes the main entity's cache tags, since this entity references it.
$this->entity->getCacheTags(),
$this->getAdditionalCacheTagsForEntity($this->entity),
$view_cache_tag,
['rendered']
);
$non_referencing_entity_cache_tags = Cache::mergeTags(
$this->nonReferencingEntity->getCacheTags(),
\Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags(),
['rendered']
);
// Generate the cache tags for all two possible entity listing paths.
// 1. list cache tag only (listing query has no match)
// 2. list cache tag plus entity cache tag (listing query has a match)
$empty_entity_listing_cache_tags = Cache::mergeTags(
$this->entity->getEntityType()->getListCacheTags(),
$page_cache_tags
);
$nonempty_entity_listing_cache_tags = Cache::mergeTags(
$this->entity->getEntityType()->getListCacheTags(),
$this->entity->getCacheTags(),
$this->getAdditionalCacheTagsForEntityListing($this->entity),
$page_cache_tags
);
$this->pass("Test referencing entity.", 'Debug');
$this->verifyPageCache($referencing_entity_url, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags, $page_cache_tags_referencing_entity));
// Also verify the existence of an entity render cache entry.
$cache_keys = ['entity_view', 'entity_test', $this->referencingEntity->id(), 'full'];
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
$access_cache_contexts = $this->getAccessCacheContextsForEntity($this->entity);
$redirected_cid = NULL;
if (count($access_cache_contexts)) {
$redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $this->getAdditionalCacheContextsForEntity($this->referencingEntity), $access_cache_contexts));
}
$this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid);
$this->pass("Test non-referencing entity.", 'Debug');
$this->verifyPageCache($non_referencing_entity_url, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($non_referencing_entity_url, 'HIT', Cache::mergeTags($non_referencing_entity_cache_tags, $page_cache_tags));
// Also verify the existence of an entity render cache entry.
$cache_keys = ['entity_view', 'entity_test', $this->nonReferencingEntity->id(), 'full'];
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
$this->verifyRenderCache($cid, $non_referencing_entity_cache_tags);
$this->pass("Test listing of referencing entities.", 'Debug');
// Prime the page cache for the listing of referencing entities.
$this->verifyPageCache($listing_url, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($listing_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags, $page_cache_tags_referencing_entity));
$this->pass("Test empty listing.", 'Debug');
// Prime the page cache for the empty listing.
$this->verifyPageCache($empty_entity_listing_url, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
// Verify the entity type's list cache contexts are present.
$contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
$this->assertEqual(Cache::mergeContexts($default_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
$this->pass("Test listing containing referenced entity.", 'Debug');
// Prime the page cache for the listing containing the referenced entity.
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS', $nonempty_entity_listing_cache_tags);
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT', $nonempty_entity_listing_cache_tags);
// Verify the entity type's list cache contexts are present.
$contexts_in_header = $this->drupalGetHeader('X-Drupal-Cache-Contexts');
$this->assertEqual(Cache::mergeContexts($default_cache_contexts, $this->getAdditionalCacheContextsForEntityListing()), empty($contexts_in_header) ? [] : explode(' ', $contexts_in_header));
// Verify that after modifying the referenced entity, there is a cache miss
// for every route except the one for the non-referencing entity.
$this->pass("Test modification of referenced entity.", 'Debug');
$this->entity->save();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($empty_entity_listing_url, 'MISS');
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify that after modifying the referencing entity, there is a cache miss
// for every route except the ones for the non-referencing entity and the
// empty entity listing.
$this->pass("Test modification of referencing entity.", 'Debug');
$this->referencingEntity->save();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify that after modifying the non-referencing entity, there is a cache
// miss only for the non-referencing entity route.
$this->pass("Test modification of non-referencing entity.", 'Debug');
$this->nonReferencingEntity->save();
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
$this->verifyPageCache($non_referencing_entity_url, 'MISS');
// Verify cache hits.
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
if ($this->entity->getEntityType()->hasHandlerClass('view_builder')) {
// Verify that after modifying the entity's display, there is a cache miss
// for both the referencing entity, and the listing of referencing
// entities, but not for any other routes.
$referenced_entity_view_mode = $this->selectViewMode($this->entity->getEntityTypeId());
$this->pass("Test modification of referenced entity's '$referenced_entity_view_mode' display.", 'Debug');
$entity_display = entity_get_display($entity_type, $this->entity->bundle(), $referenced_entity_view_mode);
$entity_display->save();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
}
$bundle_entity_type = $this->entity->getEntityType()->getBundleEntityType();
if ($bundle_entity_type !== 'bundle') {
// Verify that after modifying the corresponding bundle entity, there is a
// cache miss for both the referencing entity, and the listing of
// referencing entities, but not for any other routes.
$this->pass("Test modification of referenced entity's bundle entity.", 'Debug');
$bundle_entity = entity_load($bundle_entity_type, $this->entity->bundle());
$bundle_entity->save();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
// Special case: entity types may choose to use their bundle entity type
// cache tags, to avoid having excessively granular invalidation.
$is_special_case = $bundle_entity->getCacheTags() == $this->entity->getCacheTags() && $bundle_entity->getEntityType()->getListCacheTags() == $this->entity->getEntityType()->getListCacheTags();
if ($is_special_case) {
$this->verifyPageCache($empty_entity_listing_url, 'MISS');
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
}
else {
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
}
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
if ($is_special_case) {
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
}
}
if ($this->entity->getEntityType()->get('field_ui_base_route')) {
// Verify that after modifying a configurable field on the entity, there
// is a cache miss.
$this->pass("Test modification of referenced entity's configurable field.", 'Debug');
$field_storage_name = $this->entity->getEntityTypeId() . '.configurable_field';
$field_storage = FieldStorageConfig::load($field_storage_name);
$field_storage->save();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
// Verify that after modifying a configurable field on the entity, there
// is a cache miss.
$this->pass("Test modification of referenced entity's configurable field.", 'Debug');
$field_name = $this->entity->getEntityTypeId() . '.' . $this->entity->bundle() . '.configurable_field';
$field = FieldConfig::load($field_name);
$field->save();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
}
// Verify that after invalidating the entity's cache tag directly, there is
// a cache miss for every route except the ones for the non-referencing
// entity and the empty entity listing.
$this->pass("Test invalidation of referenced entity's cache tag.", 'Debug');
Cache::invalidateTags($this->entity->getCacheTags());
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify that after invalidating the entity's list cache tag directly,
// there is a cache miss for both the empty entity listing and the non-empty
// entity listing routes, but not for other routes.
$this->pass("Test invalidation of referenced entity's list cache tag.", 'Debug');
Cache::invalidateTags($this->entity->getEntityType()->getListCacheTags());
$this->verifyPageCache($empty_entity_listing_url, 'MISS');
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
if (!empty($view_cache_tag)) {
// Verify that after invalidating the generic entity type's view cache tag
// directly, there is a cache miss for both the referencing entity, and the
// listing of referencing entities, but not for other routes.
$this->pass("Test invalidation of referenced entity's 'view' cache tag.", 'Debug');
Cache::invalidateTags($view_cache_tag);
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
$this->verifyPageCache($empty_entity_listing_url, 'HIT');
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT');
// Verify cache hits.
$this->verifyPageCache($referencing_entity_url, 'HIT');
$this->verifyPageCache($listing_url, 'HIT');
}
// Verify that after deleting the entity, there is a cache miss for every
// route except for the non-referencing entity one.
$this->pass('Test deletion of referenced entity.', 'Debug');
$this->entity->delete();
$this->verifyPageCache($referencing_entity_url, 'MISS');
$this->verifyPageCache($listing_url, 'MISS');
$this->verifyPageCache($empty_entity_listing_url, 'MISS');
$this->verifyPageCache($nonempty_entity_listing_url, 'MISS');
$this->verifyPageCache($non_referencing_entity_url, 'HIT');
// Verify cache hits.
$referencing_entity_cache_tags = Cache::mergeTags(
$this->referencingEntity->getCacheTags(),
\Drupal::entityManager()->getViewBuilder('entity_test')->getCacheTags(),
['rendered']
);
$this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags));
$this->verifyPageCache($listing_url, 'HIT', $page_cache_tags);
$this->verifyPageCache($empty_entity_listing_url, 'HIT', $empty_entity_listing_cache_tags);
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT', Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->getAdditionalCacheTagsForEntityListing(), $page_cache_tags));
}
/**
* Creates a cache ID from a list of cache keys and a set of cache contexts.
*
* @param string[] $keys
* A list of cache keys.
* @param string[] $contexts
* A set of cache contexts.
*
* @return string
* The cache ID string.
*/
protected function createCacheId(array $keys, array $contexts) {
$cid_parts = $keys;
$contexts = \Drupal::service('cache_contexts_manager')->convertTokensToKeys($contexts);
$cid_parts = array_merge($cid_parts, $contexts);
return implode(':', $cid_parts);
}
/**
* Verify that a given render cache entry exists, with the correct cache tags.
*
* @param string $cid
* The render cache item ID.
* @param array $tags
* An array of expected cache tags.
* @param string|null $redirected_cid
* (optional) The redirected render cache item ID.
*/
protected function verifyRenderCache($cid, array $tags, $redirected_cid = NULL) {
// Also verify the existence of an entity render cache entry.
$cache_entry = \Drupal::cache('render')->get($cid);
$this->assertTrue($cache_entry, 'A render cache entry exists.');
sort($cache_entry->tags);
sort($tags);
$this->assertIdentical($cache_entry->tags, $tags);
$is_redirecting_cache_item = isset($cache_entry->data['#cache_redirect']);
if ($redirected_cid === NULL) {
$this->assertFalse($is_redirecting_cache_item, 'Render cache entry is not a redirect.');
// If this is a redirecting cache item unlike we expected, log it.
if ($is_redirecting_cache_item) {
debug($cache_entry->data);
}
}
else {
// Verify that $cid contains a cache redirect.
$this->assertTrue($is_redirecting_cache_item, 'Render cache entry is a redirect.');
// If this is not a redirecting cache item unlike we expected, log it.
if (!$is_redirecting_cache_item) {
debug($cache_entry->data);
}
// Verify that the cache redirect points to the expected CID.
$redirect_cache_metadata = $cache_entry->data['#cache'];
$actual_redirection_cid = $this->createCacheId(
$redirect_cache_metadata['keys'],
$redirect_cache_metadata['contexts']
);
$this->assertIdentical($redirected_cid, $actual_redirection_cid);
// Finally, verify that the redirected CID exists and has the same cache
// tags.
$this->verifyRenderCache($redirected_cid, $tags);
}
}
}

Some files were not shown because too many files have changed in this diff Show more