3 namespace Drupal\redirect\Tests;
5 use Drupal\Component\Utility\SafeMarkup;
6 use Drupal\Core\Language\Language;
7 use Drupal\Core\Logger\RfcLogLevel;
9 use Drupal\simpletest\WebTestBase;
12 * UI tests for redirect module.
16 class RedirectUITest extends WebTestBase {
18 use AssertRedirectTrait;
21 * @var \Drupal\Core\Session\AccountInterface
26 * @var \Drupal\redirect\RedirectRepository
28 protected $repository;
31 * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
38 public static $modules = ['redirect', 'node', 'path', 'dblog', 'views', 'taxonomy'];
43 protected function setUp() {
46 $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
47 $this->adminUser = $this->drupalCreateUser(array(
48 'administer redirects',
49 'administer redirect settings',
53 'administer taxonomy',
54 'administer url aliases',
57 $this->repository = \Drupal::service('redirect.repository');
59 $this->storage = $this->container->get('entity.manager')->getStorage('redirect');
63 * Test the redirect UI.
65 public function testRedirectUI() {
66 $this->drupalLogin($this->adminUser);
68 // Test populating the redirect form with predefined values.
69 $this->drupalGet('admin/config/search/redirect/add', array('query' => array(
70 'source' => 'non-existing',
71 'source_query' => array('key' => 'val', 'key1' => 'val1'),
73 'redirect_options' => array('query' => array('key' => 'val', 'key1' => 'val1')),
75 $this->assertFieldByName('redirect_source[0][path]', 'non-existing?key=val&key1=val1');
76 $this->assertFieldByName('redirect_redirect[0][uri]', '/node?key=val&key1=val1');
78 // Test creating a new redirect via UI.
79 $this->drupalPostForm('admin/config/search/redirect/add', array(
80 'redirect_source[0][path]' => 'non-existing',
81 'redirect_redirect[0][uri]' => '/node',
84 // Try to find the redirect we just created.
85 $redirect = $this->repository->findMatchingRedirect('non-existing');
86 $this->assertEqual($redirect->getSourceUrl(), Url::fromUri('base:non-existing')->toString());
87 $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node')->toString());
89 // After adding the redirect we should end up in the list. Check if the
90 // redirect is listed.
91 $this->assertUrl('admin/config/search/redirect');
92 $this->assertText('non-existing');
93 $this->assertLink(Url::fromUri('base:node')->toString());
94 $this->assertText(t('Not specified'));
96 // Test the edit form and update action.
97 $this->clickLink(t('Edit'));
98 $this->assertFieldByName('redirect_source[0][path]', 'non-existing');
99 $this->assertFieldByName('redirect_redirect[0][uri]', '/node');
100 $this->assertFieldByName('status_code', $redirect->getStatusCode());
102 // Append a query string to see if we handle query data properly.
103 $this->drupalPostForm(NULL, array(
104 'redirect_source[0][path]' => 'non-existing?key=value',
107 // Check the location after update and check if the value has been updated
109 $this->assertUrl('admin/config/search/redirect');
110 $this->assertText('non-existing?key=value');
112 // The path field should not contain the query string and therefore we
113 // should be able to load the redirect using only the url part without
115 $this->storage->resetCache();
116 $redirects = $this->repository->findBySourcePath('non-existing');
117 $redirect = array_shift($redirects);
118 $this->assertEqual($redirect->getSourceUrl(), Url::fromUri('base:non-existing', ['query' => ['key' => 'value']])->toString());
120 // Test the source url hints.
121 // The hint about an existing base path.
122 $this->drupalPostAjaxForm('admin/config/search/redirect/add', array(
123 'redirect_source[0][path]' => 'non-existing?key=value',
124 ), 'redirect_source[0][path]');
125 $this->assertRaw(t('The base source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?',
126 array('%source' => 'non-existing?key=value', '@edit-page' => $redirect->url('edit-form'))));
128 // The hint about a valid path.
129 $this->drupalPostAjaxForm('admin/config/search/redirect/add', array(
130 'redirect_source[0][path]' => 'node',
131 ), 'redirect_source[0][path]');
132 $this->assertRaw(t('The source path %path is likely a valid path. It is preferred to <a href="@url-alias">create URL aliases</a> for existing paths rather than redirects.',
133 array('%path' => 'node', '@url-alias' => Url::fromRoute('path.admin_add')->toString())));
136 // Duplicate redirect.
137 $this->drupalPostForm('admin/config/search/redirect/add', array(
138 'redirect_source[0][path]' => 'non-existing?key=value',
139 'redirect_redirect[0][uri]' => '/node',
141 $this->assertRaw(t('The source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?',
142 array('%source' => 'non-existing?key=value', '@edit-page' => $redirect->url('edit-form'))));
144 // Redirecting to itself.
145 $this->drupalPostForm('admin/config/search/redirect/add', array(
146 'redirect_source[0][path]' => 'node',
147 'redirect_redirect[0][uri]' => '/node',
149 $this->assertRaw(t('You are attempting to redirect the page to itself. This will result in an infinite loop.'));
151 // Redirecting the front page.
152 $this->drupalPostForm('admin/config/search/redirect/add', array(
153 'redirect_source[0][path]' => '<front>',
154 'redirect_redirect[0][uri]' => '/node',
156 $this->assertRaw(t('It is not allowed to create a redirect from the front page.'));
158 // Redirecting a url with fragment.
159 $this->drupalPostForm('admin/config/search/redirect/add', array(
160 'redirect_source[0][path]' => 'page-to-redirect#content',
161 'redirect_redirect[0][uri]' => '/node',
163 $this->assertRaw(t('The anchor fragments are not allowed.'));
165 // Adding path that starts with /
166 $this->drupalPostForm('admin/config/search/redirect/add', array(
167 'redirect_source[0][path]' => '/page-to-redirect',
168 'redirect_redirect[0][uri]' => '/node',
170 $this->assertRaw(t('The url to redirect from should not start with a forward slash (/).'));
173 // Add a new redirect.
174 $this->drupalPostForm('admin/config/search/redirect/add', array(
175 'redirect_source[0][path]' => 'test27',
176 'redirect_redirect[0][uri]' => '/node',
179 // Filter with non existing value.
180 $this->drupalGet('admin/config/search/redirect', array(
182 'status_code' => '3',
186 $rows = $this->xpath('//tbody/tr');
187 // Check if the list has no rows.
188 $this->assertTrue(count($rows) == 0);
190 // Filter with existing values.
191 $this->drupalGet('admin/config/search/redirect', array(
193 'redirect_source__path' => 'test',
194 'status_code' => '2',
198 $rows = $this->xpath('//tbody/tr');
199 // Check if the list has 1 row.
200 $this->assertTrue(count($rows) == 1);
202 $this->drupalGet('admin/config/search/redirect', array(
204 'redirect_redirect__uri' => 'nod',
208 $rows = $this->xpath('//tbody/tr');
209 // Check if the list has 2 rows.
210 $this->assertTrue(count($rows) == 2);
212 // Test the plural form of the bulk delete action.
213 $this->drupalGet('admin/config/search/redirect');
215 'redirect_bulk_form[0]' => TRUE,
216 'redirect_bulk_form[1]' => TRUE,
218 $this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
219 $this->assertText('Are you sure you want to delete these redirects?');
220 $this->clickLink('Cancel');
222 // Test the delete action.
223 $this->clickLink(t('Delete'));
224 $this->assertRaw(t('Are you sure you want to delete the URL redirect from %source to %redirect?',
225 array('%source' => Url::fromUri('base:non-existing', ['query' => ['key' => 'value']])->toString(), '%redirect' => Url::fromUri('base:node')->toString())));
226 $this->drupalPostForm(NULL, array(), t('Delete'));
227 $this->assertUrl('admin/config/search/redirect');
229 // Test the bulk delete action.
230 $this->drupalPostForm(NULL, ['redirect_bulk_form[0]' => TRUE], t('Apply to selected items'));
231 $this->assertText('Are you sure you want to delete this redirect?');
232 $this->assertText('test27');
233 $this->drupalPostForm(NULL, [], t('Delete'));
235 $this->assertText(t('There is no redirect yet.'));
239 * Tests redirects being automatically created upon path alias change.
241 public function testAutomaticRedirects() {
242 $this->drupalLogin($this->adminUser);
244 // Create a node and update its path alias which should result in a redirect
245 // being automatically created from the old alias to the new one.
246 $node = $this->drupalCreateNode(array(
248 'langcode' => Language::LANGCODE_NOT_SPECIFIED,
249 'path' => array('alias' => '/node_test_alias'),
251 $this->drupalGet('node/' . $node->id() . '/edit');
252 $this->assertText(t('No URL redirects available.'));
253 $this->drupalPostForm('node/' . $node->id() . '/edit', array('path[0][alias]' => '/node_test_alias_updated'), t('Save'));
255 $redirect = $this->repository->findMatchingRedirect('node_test_alias', array(), Language::LANGCODE_NOT_SPECIFIED);
256 $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node_test_alias_updated')->toString());
257 // Test if the automatically created redirect works.
258 $this->assertRedirect('node_test_alias', 'node_test_alias_updated');
260 // Test that changing the path back deletes the first redirect, creates
261 // a new one and does not result in a loop.
262 $this->drupalPostForm('node/' . $node->id() . '/edit', array('path[0][alias]' => '/node_test_alias'), t('Save'));
263 $redirect = $this->repository->findMatchingRedirect('node_test_alias', array(), Language::LANGCODE_NOT_SPECIFIED);
264 $this->assertTrue(empty($redirect));
266 \Drupal::service('path.alias_manager')->cacheClear();
267 $redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', array(), Language::LANGCODE_NOT_SPECIFIED);
269 $this->drupalGet('node/' . $node->id() . '/edit');
270 $this->assertText($redirect->getSourcePathWithQuery());
271 $this->assertLinkByHref(Url::fromRoute('entity.redirect.edit_form', ['redirect' => $redirect->id()])->toString());
272 $this->assertLinkByHref(Url::fromRoute('entity.redirect.delete_form', ['redirect' => $redirect->id()])->toString());
274 $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node_test_alias')->toString());
275 // Test if the automatically created redirect works.
276 $this->assertRedirect('node_test_alias_updated', 'node_test_alias');
278 // Test that the redirect will be deleted upon node deletion.
279 $this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete'));
280 $redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', array(), Language::LANGCODE_NOT_SPECIFIED);
281 $this->assertTrue(empty($redirect));
283 // Create a term and update its path alias and check if we have a redirect
284 // from the previous path alias to the new one.
285 $term = $this->createTerm($this->createVocabulary());
286 $this->drupalPostForm('taxonomy/term/' . $term->id() . '/edit', array('path[0][alias]' => '/term_test_alias_updated'), t('Save'));
287 $redirect = $this->repository->findMatchingRedirect('term_test_alias');
288 $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:term_test_alias_updated')->toString());
289 // Test if the automatically created redirect works.
290 $this->assertRedirect('term_test_alias', 'term_test_alias_updated');
292 // Test the path alias update via the admin path form.
293 $this->drupalPostForm('admin/config/search/path/add', array(
295 'alias' => '/aaa_path_alias',
297 // Note that here we rely on fact that we land on the path alias list page
298 // and the default sort is by the alias, which implies that the first edit
299 // link leads to the edit page of the aaa_path_alias.
300 $this->clickLink(t('Edit'));
301 $this->drupalPostForm(NULL, array('alias' => '/aaa_path_alias_updated'), t('Save'));
302 $redirect = $this->repository->findMatchingRedirect('aaa_path_alias', array(), 'en');
303 $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:aaa_path_alias_updated')->toString());
304 // Test if the automatically created redirect works.
305 $this->assertRedirect('aaa_path_alias', 'aaa_path_alias_updated');
307 // Test the automatically created redirect shows up in the form correctly.
308 $this->drupalGet('admin/config/search/redirect/edit/' . $redirect->id());
309 $this->assertFieldByName('redirect_source[0][path]', 'aaa_path_alias');
310 $this->assertFieldByName('redirect_redirect[0][uri]', '/node');
314 * Test the redirect loop protection and logging.
316 function testRedirectLoop() {
317 // Redirect loop redirection only works when page caching is disabled.
318 \Drupal::service('module_installer')->uninstall(['page_cache']);
320 /** @var \Drupal\redirect\Entity\Redirect $redirect1 */
321 $redirect1 = $this->storage->create();
322 $redirect1->setSource('node');
323 $redirect1->setRedirect('admin');
324 $redirect1->setStatusCode(301);
327 /** @var \Drupal\redirect\Entity\Redirect $redirect2 */
328 $redirect2 = $this->storage->create();
329 $redirect2->setSource('admin');
330 $redirect2->setRedirect('node');
331 $redirect2->setStatusCode(301);
334 $this->maximumRedirects = 10;
335 $this->drupalGet('node');
336 $this->assertText('Service unavailable');
337 $this->assertResponse(503);
339 $log = db_select('watchdog')->fields('watchdog')->condition('type', 'redirect')->execute()->fetchAll();
340 if (count($log) == 0) {
341 $this->fail('Redirect loop has not been logged');
345 $this->assertEqual($log->severity, RfcLogLevel::WARNING);
346 $this->assertEqual(SafeMarkup::format($log->message, unserialize($log->variables)),
347 SafeMarkup::format('Redirect loop identified at %path for redirect %id', array('%path' => '/node', '%id' => $redirect1->id())));
352 * Returns a new vocabulary with random properties.
354 function createVocabulary() {
355 // Create a vocabulary.
356 $vocabulary = entity_create('taxonomy_vocabulary', array(
357 'name' => $this->randomMachineName(),
358 'description' => $this->randomMachineName(),
359 'vid' => mb_strtolower($this->randomMachineName()),
360 'langcode' => Language::LANGCODE_NOT_SPECIFIED,
361 'weight' => mt_rand(0, 10),
368 * Returns a new term with random properties in vocabulary $vid.
370 function createTerm($vocabulary) {
371 $filter_formats = filter_formats();
372 $format = array_pop($filter_formats);
373 $term = entity_create('taxonomy_term', array(
374 'name' => $this->randomMachineName(),
375 'description' => array(
376 'value' => $this->randomMachineName(),
377 // Use the first available text format.
378 'format' => $format->id(),
380 'vid' => $vocabulary->id(),
381 'langcode' => Language::LANGCODE_NOT_SPECIFIED,
382 'path' => array('alias' => '/term_test_alias'),
391 * @todo Not sure this belongs in a UI test, but a full web test is needed.
393 public function testCacheTags() {
394 /** @var \Drupal\redirect\Entity\Redirect $redirect1 */
395 $redirect1 = $this->storage->create();
396 $redirect1->setSource('test-redirect');
397 $redirect1->setRedirect('node');
398 $redirect1->setStatusCode(301);
401 $this->assertRedirect('test-redirect', 'node');
402 $headers = $this->drupalGetHeaders(TRUE);
403 // Note, self::assertCacheTag() cannot be used here since it only looks at
404 // the final set of headers.
405 $expected = 'http_response ' . implode(' ', $redirect1->getCacheTags());
406 $this->assertEqual($expected, $headers[0]['x-drupal-cache-tags'], 'Redirect cache tags properly set.');
408 // First request should be a cache MISS.
409 $this->assertEqual($headers[0]['x-drupal-cache'], 'MISS', 'First request to the redirect was not cached.');
411 // Second request should be cached.
412 $this->assertRedirect('test-redirect', 'node');
413 $headers = $this->drupalGetHeaders(TRUE);
414 $this->assertEqual($headers[0]['x-drupal-cache'], 'HIT', 'The second request to the redirect was cached.');
416 // Ensure that the redirect has been cleared from cache when deleted.
417 $redirect1->delete();
418 $this->drupalGet('test-redirect');
419 $this->assertResponse(404, 'Deleted redirect properly clears the internal page cache.');
423 * Test external destinations.
425 public function testExternal() {
426 $redirect = $this->storage->create();
427 $redirect->setSource('a-path');
428 // @todo Redirect::setRedirect() assumes that all redirects are internal.
429 $redirect->redirect_redirect->set(0, ['uri' => 'https://www.example.org']);
430 $redirect->setStatusCode(301);
432 $this->assertRedirect('a-path', 'https://www.example.org');
433 $this->drupalLogin($this->adminUser);