1<?php 2 3/** 4 * @file 5 * Tests for the Path module. 6 */ 7 8/** 9 * Provides a base class for testing the Path module. 10 */ 11class PathTestCase extends DrupalWebTestCase { 12 public static function getInfo() { 13 return array( 14 'name' => 'Path alias functionality', 15 'description' => 'Add, edit, delete, and change alias and verify its consistency in the database.', 16 'group' => 'Path', 17 ); 18 } 19 20 function setUp() { 21 parent::setUp('path'); 22 23 // Create test user and login. 24 $web_user = $this->drupalCreateUser(array('create page content', 'edit own page content', 'administer url aliases', 'create url aliases', 'access content overview')); 25 $this->drupalLogin($web_user); 26 } 27 28 /** 29 * Tests the path cache. 30 */ 31 function testPathCache() { 32 // Create test node. 33 $node1 = $this->drupalCreateNode(); 34 35 // Create alias. 36 $edit = array(); 37 $edit['source'] = 'node/' . $node1->nid; 38 $edit['alias'] = $this->randomName(8); 39 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 40 41 // Visit the system path for the node and confirm a cache entry is 42 // created. 43 cache_clear_all('*', 'cache_path', TRUE); 44 $this->drupalGet($edit['source']); 45 $this->assertTrue(cache_get($edit['source'], 'cache_path'), 'Cache entry was created.'); 46 47 // Visit the alias for the node and confirm a cache entry is created. 48 cache_clear_all('*', 'cache_path', TRUE); 49 $this->drupalGet($edit['alias']); 50 $this->assertTrue(cache_get($edit['source'], 'cache_path'), 'Cache entry was created.'); 51 } 52 53 /** 54 * Tests alias functionality through the admin interfaces. 55 */ 56 function testAdminAlias() { 57 // Create test node. 58 $node1 = $this->drupalCreateNode(); 59 60 // Create alias. 61 $edit = array(); 62 $edit['source'] = 'node/' . $node1->nid; 63 $edit['alias'] = $this->randomName(8); 64 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 65 66 // Confirm that the alias works. 67 $this->drupalGet($edit['alias']); 68 $this->assertText($node1->title, 'Alias works.'); 69 $this->assertResponse(200); 70 71 // Change alias to one containing "exotic" characters. 72 $pid = $this->getPID($edit['alias']); 73 74 $previous = $edit['alias']; 75 $edit['alias'] = "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. 76 "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. 77 "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. 78 $this->drupalPost('admin/config/search/path/edit/' . $pid, $edit, t('Save')); 79 80 // Confirm that the alias works. 81 $this->drupalGet($edit['alias']); 82 $this->assertText($node1->title, 'Changed alias works.'); 83 $this->assertResponse(200); 84 85 drupal_static_reset('drupal_lookup_path'); 86 // Confirm that previous alias no longer works. 87 $this->drupalGet($previous); 88 $this->assertNoText($node1->title, 'Previous alias no longer works.'); 89 $this->assertResponse(404); 90 91 // Create second test node. 92 $node2 = $this->drupalCreateNode(); 93 94 // Set alias to second test node. 95 $edit['source'] = 'node/' . $node2->nid; 96 // leave $edit['alias'] the same 97 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 98 99 // Confirm no duplicate was created. 100 $this->assertRaw(t('The alias %alias is already in use in this language.', array('%alias' => $edit['alias'])), 'Attempt to move alias was rejected.'); 101 102 // Delete alias. 103 $this->drupalPost('admin/config/search/path/edit/' . $pid, array(), t('Delete')); 104 $this->drupalPost(NULL, array(), t('Confirm')); 105 106 // Confirm that the alias no longer works. 107 $this->drupalGet($edit['alias']); 108 $this->assertNoText($node1->title, 'Alias was successfully deleted.'); 109 $this->assertResponse(404); 110 111 // Create third and fourth test node. 112 $node3 = $this->drupalCreateNode(); 113 $node4 = $this->drupalCreateNode(); 114 115 // Give the node aliases a common first part. 116 $name = $this->randomName(4); 117 118 // Create aliases containing a slash. 119 $edit = array(); 120 $edit['source'] = 'node/' . $node3->nid; 121 $alias3 = $name . '/' . $this->randomName(5); 122 $edit['alias'] = $alias3; 123 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 124 $edit['source'] = 'node/' . $node4->nid; 125 $alias4 = $name . '/' . $this->randomName(4); 126 $edit['alias'] = $alias4; 127 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 128 129 // Confirm that the aliases work. 130 $this->drupalGet($alias3); 131 $this->assertText($node3->title, 'Alias works.'); 132 $this->assertResponse(200); 133 $this->drupalGet($alias4); 134 $this->assertText($node4->title, 'Alias works.'); 135 $this->assertResponse(200); 136 137 // Confirm that filters containing slashes work. 138 $this->drupalGet('admin/config/search/path/list/' . $alias3); 139 $this->assertFieldByName('filter', $alias3); 140 $this->assertText($alias3, 'Searched-for alias with slash found.'); 141 $this->assertNoText($alias4, 'Different alias with slash not found.'); 142 $this->assertResponse(200); 143 144 // Delete aliases. 145 $pid = $this->getPID($alias3); 146 $this->drupalPost('admin/config/search/path/edit/' . $pid, array(), t('Delete')); 147 $this->drupalPost(NULL, array(), t('Confirm')); 148 $pid = $this->getPID($alias4); 149 $this->drupalPost('admin/config/search/path/edit/' . $pid, array(), t('Delete')); 150 $this->drupalPost(NULL, array(), t('Confirm')); 151 152 // Confirm that the aliases no longer work. 153 $this->drupalGet($alias3); 154 $this->assertNoText($node3->title, 'Alias was successfully deleted.'); 155 $this->assertResponse(404); 156 $this->drupalGet($alias4); 157 $this->assertNoText($node4->title, 'Alias was successfully deleted.'); 158 $this->assertResponse(404); 159 } 160 161 /** 162 * Tests alias functionality through the node interfaces. 163 */ 164 function testNodeAlias() { 165 // Create test node. 166 $node1 = $this->drupalCreateNode(); 167 168 // Create alias. 169 $edit = array(); 170 $edit['path[alias]'] = $this->randomName(8); 171 $this->drupalPost('node/' . $node1->nid . '/edit', $edit, t('Save')); 172 173 // Confirm that the alias works. 174 $this->drupalGet($edit['path[alias]']); 175 $this->assertText($node1->title, 'Alias works.'); 176 $this->assertResponse(200); 177 178 // Change alias to one containing "exotic" characters. 179 $previous = $edit['path[alias]']; 180 $edit['path[alias]'] = "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. 181 "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. 182 "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. 183 $this->drupalPost('node/' . $node1->nid . '/edit', $edit, t('Save')); 184 185 // Confirm that the alias works. 186 $this->drupalGet($edit['path[alias]']); 187 $this->assertText($node1->title, 'Changed alias works.'); 188 $this->assertResponse(200); 189 190 // Make sure that previous alias no longer works. 191 $this->drupalGet($previous); 192 $this->assertNoText($node1->title, 'Previous alias no longer works.'); 193 $this->assertResponse(404); 194 195 // Create second test node. 196 $node2 = $this->drupalCreateNode(); 197 198 // Set alias to second test node. 199 // Leave $edit['path[alias]'] the same. 200 $this->drupalPost('node/' . $node2->nid . '/edit', $edit, t('Save')); 201 202 // Confirm that the alias didn't make a duplicate. 203 $this->assertText(t('The alias is already in use.'), 'Attempt to moved alias was rejected.'); 204 205 // Delete alias. 206 $this->drupalPost('node/' . $node1->nid . '/edit', array('path[alias]' => ''), t('Save')); 207 208 // Confirm that the alias no longer works. 209 $this->drupalGet($edit['path[alias]']); 210 $this->assertNoText($node1->title, 'Alias was successfully deleted.'); 211 $this->assertResponse(404); 212 213 // Create third test node. 214 $node3 = $this->drupalCreateNode(); 215 216 // Create an invalid alias with a leading slash and verify that the slash 217 // is removed when the link is generated. This ensures that URL aliases 218 // cannot be used to inject external URLs. 219 // @todo The user interface should either display an error message or 220 // automatically trim these invalid aliases, rather than allowing them to 221 // be silently created, at which point the functional aspects of this 222 // test will need to be moved elsewhere and switch to using a 223 // programmatically-created alias instead. 224 $alias = $this->randomName(8); 225 $edit = array('path[alias]' => '/' . $alias); 226 $this->drupalPost('node/' . $node3->nid . '/edit', $edit, t('Save')); 227 $this->drupalGet('admin/content'); 228 // This checks the link href before clicking it, rather than using 229 // DrupalWebTestCase::assertUrl() after clicking it, because the test 230 // browser does not always preserve the correct number of slashes in the 231 // URL when it visits internal links; using DrupalWebTestCase::assertUrl() 232 // would actually make the test pass unconditionally on the testbot (or 233 // anywhere else where Drupal is installed in a subdirectory). 234 $link_xpath = $this->xpath('//a[normalize-space(text())=:label]', array(':label' => $node3->title)); 235 $link_href = (string) $link_xpath[0]['href']; 236 $link_prefix = base_path() . (variable_get('clean_url', 0) ? '' : '?q='); 237 $this->assertEqual($link_href, $link_prefix . $alias); 238 $this->clickLink($node3->title); 239 $this->assertResponse(404); 240 } 241 242 /** 243 * Returns the path ID. 244 * 245 * @param $alias 246 * A string containing an aliased path. 247 * 248 * @return int 249 * Integer representing the path ID. 250 */ 251 function getPID($alias) { 252 return db_query("SELECT pid FROM {url_alias} WHERE alias = :alias", array(':alias' => $alias))->fetchField(); 253 } 254 255 /** 256 * Tests that duplicate aliases fail validation. 257 */ 258 function testDuplicateNodeAlias() { 259 // Create one node with a random alias. 260 $node_one = $this->drupalCreateNode(); 261 $edit = array(); 262 $edit['path[alias]'] = $this->randomName(); 263 $this->drupalPost('node/' . $node_one->nid . '/edit', $edit, t('Save')); 264 265 // Now create another node and try to set the same alias. 266 $node_two = $this->drupalCreateNode(); 267 $this->drupalPost('node/' . $node_two->nid . '/edit', $edit, t('Save')); 268 $this->assertText(t('The alias is already in use.')); 269 $this->assertFieldByXPath("//input[@name='path[alias]' and contains(@class, 'error')]", $edit['path[alias]'], 'Textfield exists and has the error class.'); 270 } 271} 272 273/** 274 * Tests URL aliases for taxonomy terms. 275 */ 276class PathTaxonomyTermTestCase extends DrupalWebTestCase { 277 public static function getInfo() { 278 return array( 279 'name' => 'Taxonomy term URL aliases', 280 'description' => 'Tests URL aliases for taxonomy terms.', 281 'group' => 'Path', 282 ); 283 } 284 285 function setUp() { 286 parent::setUp('path', 'taxonomy'); 287 288 // Create and login user. 289 $web_user = $this->drupalCreateUser(array('administer url aliases', 'administer taxonomy', 'access administration pages')); 290 $this->drupalLogin($web_user); 291 } 292 293 /** 294 * Tests alias functionality through the admin interfaces. 295 */ 296 function testTermAlias() { 297 // Create a term in the default 'Tags' vocabulary with URL alias. 298 $vocabulary = taxonomy_vocabulary_load(1); 299 $description = $this->randomName();; 300 $edit = array(); 301 $edit['name'] = $this->randomName(); 302 $edit['description[value]'] = $description; 303 $edit['path[alias]'] = $this->randomName(); 304 $this->drupalPost('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add', $edit, t('Save')); 305 306 // Confirm that the alias works. 307 $this->drupalGet($edit['path[alias]']); 308 $this->assertText($description, 'Term can be accessed on URL alias.'); 309 310 // Change the term's URL alias. 311 $tid = db_query("SELECT tid FROM {taxonomy_term_data} WHERE name = :name", array(':name' => $edit['name']))->fetchField(); 312 $edit2 = array(); 313 $edit2['path[alias]'] = $this->randomName(); 314 $this->drupalPost('taxonomy/term/' . $tid . '/edit', $edit2, t('Save')); 315 316 // Confirm that the changed alias works. 317 $this->drupalGet($edit2['path[alias]']); 318 $this->assertText($description, 'Term can be accessed on changed URL alias.'); 319 320 // Confirm that the old alias no longer works. 321 $this->drupalGet($edit['path[alias]']); 322 $this->assertNoText($description, 'Old URL alias has been removed after altering.'); 323 $this->assertResponse(404, 'Old URL alias returns 404.'); 324 325 // Remove the term's URL alias. 326 $edit3 = array(); 327 $edit3['path[alias]'] = ''; 328 $this->drupalPost('taxonomy/term/' . $tid . '/edit', $edit3, t('Save')); 329 330 // Confirm that the alias no longer works. 331 $this->drupalGet($edit2['path[alias]']); 332 $this->assertNoText($description, 'Old URL alias has been removed after altering.'); 333 $this->assertResponse(404, 'Old URL alias returns 404.'); 334 } 335} 336 337/** 338 * Tests URL aliases for translated nodes. 339 */ 340class PathLanguageTestCase extends DrupalWebTestCase { 341 public static function getInfo() { 342 return array( 343 'name' => 'Path aliases with translated nodes', 344 'description' => 'Confirm that paths work with translated nodes', 345 'group' => 'Path', 346 ); 347 } 348 349 function setUp() { 350 parent::setUp('path', 'locale', 'translation'); 351 352 // Create and login user. 353 $this->web_user = $this->drupalCreateUser(array('edit any page content', 'create page content', 'administer url aliases', 'create url aliases', 'administer languages', 'translate content', 'access administration pages')); 354 $this->drupalLogin($this->web_user); 355 356 // Enable French language. 357 $edit = array(); 358 $edit['langcode'] = 'fr'; 359 360 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); 361 362 // Enable URL language detection and selection. 363 $edit = array('language[enabled][locale-url]' => 1); 364 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); 365 } 366 367 /** 368 * Test alias functionality through the admin interfaces. 369 */ 370 function testAliasTranslation() { 371 // Set 'page' content type to enable translation. 372 variable_set('language_content_type_page', 2); 373 374 $english_node = $this->drupalCreateNode(array('type' => 'page')); 375 $english_alias = $this->randomName(); 376 377 // Edit the node to set language and path. 378 $edit = array(); 379 $edit['language'] = 'en'; 380 $edit['path[alias]'] = $english_alias; 381 $this->drupalPost('node/' . $english_node->nid . '/edit', $edit, t('Save')); 382 383 // Confirm that the alias works. 384 $this->drupalGet($english_alias); 385 $this->assertText($english_node->title, 'Alias works.'); 386 387 // Translate the node into French. 388 $this->drupalGet('node/' . $english_node->nid . '/translate'); 389 $this->clickLink(t('add translation')); 390 $edit = array(); 391 $langcode = LANGUAGE_NONE; 392 $edit["title"] = $this->randomName(); 393 $edit["body[$langcode][0][value]"] = $this->randomName(); 394 $french_alias = $this->randomName(); 395 $edit['path[alias]'] = $french_alias; 396 $this->drupalPost(NULL, $edit, t('Save')); 397 398 // Clear the path lookup cache. 399 drupal_lookup_path('wipe'); 400 401 // Ensure the node was created. 402 $french_node = $this->drupalGetNodeByTitle($edit["title"]); 403 $this->assertTrue(($french_node), 'Node found in database.'); 404 405 // Confirm that the alias works. 406 $this->drupalGet('fr/' . $edit['path[alias]']); 407 $this->assertText($french_node->title, 'Alias for French translation works.'); 408 409 // Confirm that the alias is returned by url(). 410 drupal_static_reset('language_list'); 411 drupal_static_reset('locale_url_outbound_alter'); 412 $languages = language_list(); 413 $url = url('node/' . $french_node->nid, array('language' => $languages[$french_node->language])); 414 $this->assertTrue(strpos($url, $edit['path[alias]']), 'URL contains the path alias.'); 415 416 // Confirm that the alias works even when changing language negotiation 417 // options. Enable User language detection and selection over URL one. 418 $edit = array( 419 'language[enabled][locale-user]' => 1, 420 'language[weight][locale-user]' => -9, 421 'language[enabled][locale-url]' => 1, 422 'language[weight][locale-url]' => -8, 423 ); 424 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); 425 426 // Change user language preference. 427 $edit = array('language' => 'fr'); 428 $this->drupalPost("user/{$this->web_user->uid}/edit", $edit, t('Save')); 429 430 // Check that the English alias works. In this situation French is the 431 // current UI and content language, while URL language is English (since we 432 // do not have a path prefix we fall back to the site's default language). 433 // We need to ensure that the user language preference is not taken into 434 // account while determining the path alias language, because if this 435 // happens we have no way to check that the path alias is valid: there is no 436 // path alias for French matching the english alias. So drupal_lookup_path() 437 // needs to use the URL language to check whether the alias is valid. 438 $this->drupalGet($english_alias); 439 $this->assertText($english_node->title, 'Alias for English translation works.'); 440 441 // Check that the French alias works. 442 $this->drupalGet("fr/$french_alias"); 443 $this->assertText($french_node->title, 'Alias for French translation works.'); 444 445 // Disable URL language negotiation. 446 $edit = array('language[enabled][locale-url]' => FALSE); 447 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); 448 449 // Check that the English alias still works. 450 $this->drupalGet($english_alias); 451 $this->assertText($english_node->title, 'Alias for English translation works.'); 452 453 // Check that the French alias is not available. We check the unprefixed 454 // alias because we disabled URL language negotiation above. In this 455 // situation only aliases in the default language and language neutral ones 456 // should keep working. 457 $this->drupalGet($french_alias); 458 $this->assertResponse(404, 'Alias for French translation is unavailable when URL language negotiation is disabled.'); 459 460 // drupal_lookup_path() has an internal static cache. Check to see that 461 // it has the appropriate contents at this point. 462 drupal_lookup_path('wipe'); 463 $french_node_path = drupal_lookup_path('source', $french_alias, $french_node->language); 464 $this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path works.'); 465 // Second call should return the same path. 466 $french_node_path = drupal_lookup_path('source', $french_alias, $french_node->language); 467 $this->assertEqual($french_node_path, 'node/' . $french_node->nid, 'Normal path is the same.'); 468 469 // Confirm that the alias works. 470 $french_node_alias = drupal_lookup_path('alias', 'node/' . $french_node->nid, $french_node->language); 471 $this->assertEqual($french_node_alias, $french_alias, 'Alias works.'); 472 // Second call should return the same alias. 473 $french_node_alias = drupal_lookup_path('alias', 'node/' . $french_node->nid, $french_node->language); 474 $this->assertEqual($french_node_alias, $french_alias, 'Alias is the same.'); 475 } 476} 477 478/** 479 * Tests the user interface for creating path aliases, with languages. 480 */ 481class PathLanguageUITestCase extends DrupalWebTestCase { 482 public static function getInfo() { 483 return array( 484 'name' => 'Path aliases with languages', 485 'description' => 'Confirm that the Path module user interface works with languages.', 486 'group' => 'Path', 487 ); 488 } 489 490 function setUp() { 491 parent::setUp('path', 'locale'); 492 493 // Create and login user. 494 $web_user = $this->drupalCreateUser(array('edit any page content', 'create page content', 'administer url aliases', 'create url aliases', 'administer languages', 'access administration pages')); 495 $this->drupalLogin($web_user); 496 497 // Enable French language. 498 $edit = array(); 499 $edit['langcode'] = 'fr'; 500 501 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); 502 503 // Enable URL language detection and selection. 504 $edit = array('language[enabled][locale-url]' => 1); 505 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); 506 } 507 508 /** 509 * Tests that a language-neutral URL alias works. 510 */ 511 function testLanguageNeutralURLs() { 512 $name = $this->randomName(8); 513 $edit = array(); 514 $edit['source'] = 'admin/config/search/path'; 515 $edit['alias'] = $name; 516 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 517 518 $this->drupalGet($name); 519 $this->assertText(t('Filter aliases'), 'Language-neutral URL alias works'); 520 } 521 522 /** 523 * Tests that a default language URL alias works. 524 */ 525 function testDefaultLanguageURLs() { 526 $name = $this->randomName(8); 527 $edit = array(); 528 $edit['source'] = 'admin/config/search/path'; 529 $edit['alias'] = $name; 530 $edit['language'] = 'en'; 531 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 532 533 $this->drupalGet($name); 534 $this->assertText(t('Filter aliases'), 'English URL alias works'); 535 } 536 537 /** 538 * Tests that a non-default language URL alias works. 539 */ 540 function testNonDefaultURLs() { 541 $name = $this->randomName(8); 542 $edit = array(); 543 $edit['source'] = 'admin/config/search/path'; 544 $edit['alias'] = $name; 545 $edit['language'] = 'fr'; 546 $this->drupalPost('admin/config/search/path/add', $edit, t('Save')); 547 548 $this->drupalGet('fr/' . $name); 549 $this->assertText(t('Filter aliases'), 'Foreign URL alias works'); 550 } 551 552} 553 554/** 555 * Tests that paths are not prefixed on a monolingual site. 556 */ 557class PathMonolingualTestCase extends DrupalWebTestCase { 558 public static function getInfo() { 559 return array( 560 'name' => 'Paths on non-English monolingual sites', 561 'description' => 'Confirm that paths are not changed on monolingual non-English sites', 562 'group' => 'Path', 563 ); 564 } 565 566 function setUp() { 567 global $language; 568 parent::setUp('path', 'locale', 'translation'); 569 570 // Create and login user. 571 $web_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); 572 $this->drupalLogin($web_user); 573 574 // Enable French language. 575 $edit = array(); 576 $edit['langcode'] = 'fr'; 577 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); 578 579 // Make French the default language. 580 $edit = array('site_default' => 'fr'); 581 $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); 582 583 // Disable English. 584 $edit = array('enabled[en]' => FALSE); 585 $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration')); 586 587 // Verify that French is the only language. 588 $this->assertFalse(drupal_multilingual(), 'Site is mono-lingual'); 589 $this->assertEqual(language_default('language'), 'fr', 'French is the default language'); 590 591 // Set language detection to URL. 592 $edit = array('language[enabled][locale-url]' => TRUE); 593 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings')); 594 595 // Force languages to be initialized. 596 drupal_language_initialize(); 597 } 598 599 /** 600 * Verifies that links do not have language prefixes in them. 601 */ 602 function testPageLinks() { 603 // Navigate to 'admin/config' path. 604 $this->drupalGet('admin/config'); 605 606 // Verify that links in this page do not have a 'fr/' prefix. 607 $this->assertNoLinkByHref('/fr/', 'Links do not contain language prefix'); 608 609 // Verify that links in this page can be followed and work. 610 $this->clickLink(t('Languages')); 611 $this->assertResponse(200, 'Clicked link results in a valid page'); 612 $this->assertText(t('Add language'), 'Page contains the add language text'); 613 } 614} 615