aggregator.test 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. <?php
  2. /**
  3. * @file
  4. * Tests for aggregator.module.
  5. */
  6. /**
  7. * Defines a base class for testing the Aggregator module.
  8. */
  9. class AggregatorTestCase extends DrupalWebTestCase {
  10. function setUp() {
  11. parent::setUp('aggregator', 'aggregator_test');
  12. $web_user = $this->drupalCreateUser(array('administer news feeds', 'access news feeds', 'create article content'));
  13. $this->drupalLogin($web_user);
  14. }
  15. /**
  16. * Creates an aggregator feed.
  17. *
  18. * This method simulates the form submission on path
  19. * admin/config/services/aggregator/add/feed.
  20. *
  21. * @param $feed_url
  22. * (optional) If given, feed will be created with this URL, otherwise
  23. * /rss.xml will be used. Defaults to NULL.
  24. *
  25. * @return $feed
  26. * Full feed object if possible.
  27. *
  28. * @see getFeedEditArray()
  29. */
  30. function createFeed($feed_url = NULL) {
  31. $edit = $this->getFeedEditArray($feed_url);
  32. $this->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save'));
  33. $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), format_string('The feed !name has been added.', array('!name' => $edit['title'])));
  34. $feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $edit['title'], ':url' => $edit['url']))->fetch();
  35. $this->assertTrue(!empty($feed), 'The feed found in database.');
  36. return $feed;
  37. }
  38. /**
  39. * Deletes an aggregator feed.
  40. *
  41. * @param $feed
  42. * Feed object representing the feed.
  43. */
  44. function deleteFeed($feed) {
  45. $this->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, array(), t('Delete'));
  46. $this->assertRaw(t('The feed %title has been deleted.', array('%title' => $feed->title)), 'Feed deleted successfully.');
  47. }
  48. /**
  49. * Returns a randomly generated feed edit array.
  50. *
  51. * @param $feed_url
  52. * (optional) If given, feed will be created with this URL, otherwise
  53. * /rss.xml will be used. Defaults to NULL.
  54. * @return
  55. * A feed array.
  56. */
  57. function getFeedEditArray($feed_url = NULL) {
  58. $feed_name = $this->randomName(10);
  59. if (!$feed_url) {
  60. $feed_url = url('rss.xml', array(
  61. 'query' => array('feed' => $feed_name),
  62. 'absolute' => TRUE,
  63. ));
  64. }
  65. $edit = array(
  66. 'title' => $feed_name,
  67. 'url' => $feed_url,
  68. 'refresh' => '900',
  69. );
  70. return $edit;
  71. }
  72. /**
  73. * Returns the count of the randomly created feed array.
  74. *
  75. * @return
  76. * Number of feed items on default feed created by createFeed().
  77. */
  78. function getDefaultFeedItemCount() {
  79. // Our tests are based off of rss.xml, so let's find out how many elements should be related.
  80. $feed_count = db_query_range('SELECT COUNT(*) FROM {node} n WHERE n.promote = 1 AND n.status = 1', 0, variable_get('feed_default_items', 10))->fetchField();
  81. return $feed_count > 10 ? 10 : $feed_count;
  82. }
  83. /**
  84. * Updates the feed items.
  85. *
  86. * This method simulates a click to
  87. * admin/config/services/aggregator/update/$fid.
  88. *
  89. * @param $feed
  90. * Feed object representing the feed, passed by reference.
  91. * @param $expected_count
  92. * Expected number of feed items.
  93. */
  94. function updateFeedItems(&$feed, $expected_count) {
  95. // First, let's ensure we can get to the rss xml.
  96. $this->drupalGet($feed->url);
  97. $this->assertResponse(200, format_string('!url is reachable.', array('!url' => $feed->url)));
  98. // Attempt to access the update link directly without an access token.
  99. $this->drupalGet('admin/config/services/aggregator/update/' . $feed->fid);
  100. $this->assertResponse(403);
  101. // Refresh the feed (simulated link click).
  102. $this->drupalGet('admin/config/services/aggregator');
  103. $this->clickLink('update items');
  104. // Ensure we have the right number of items.
  105. $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid));
  106. $items = array();
  107. $feed->items = array();
  108. foreach ($result as $item) {
  109. $feed->items[] = $item->iid;
  110. }
  111. $feed->item_count = count($feed->items);
  112. $this->assertEqual($expected_count, $feed->item_count, format_string('Total items in feed equal to the total items in database (!val1 != !val2)', array('!val1' => $expected_count, '!val2' => $feed->item_count)));
  113. }
  114. /**
  115. * Confirms an item removal from a feed.
  116. *
  117. * @param $feed
  118. * Feed object representing the feed.
  119. */
  120. function removeFeedItems($feed) {
  121. $this->drupalPost('admin/config/services/aggregator/remove/' . $feed->fid, array(), t('Remove items'));
  122. $this->assertRaw(t('The news items from %title have been removed.', array('%title' => $feed->title)), 'Feed items removed.');
  123. }
  124. /**
  125. * Adds and removes feed items and ensure that the count is zero.
  126. *
  127. * @param $feed
  128. * Feed object representing the feed.
  129. * @param $expected_count
  130. * Expected number of feed items.
  131. */
  132. function updateAndRemove($feed, $expected_count) {
  133. $this->updateFeedItems($feed, $expected_count);
  134. $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField();
  135. $this->assertTrue($count);
  136. $this->removeFeedItems($feed);
  137. $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField();
  138. $this->assertTrue($count == 0);
  139. }
  140. /**
  141. * Pulls feed categories from {aggregator_category_feed} table.
  142. *
  143. * @param $feed
  144. * Feed object representing the feed.
  145. */
  146. function getFeedCategories($feed) {
  147. // add the categories to the feed so we can use them
  148. $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $feed->fid));
  149. foreach ($result as $category) {
  150. $feed->categories[] = $category->cid;
  151. }
  152. }
  153. /**
  154. * Pulls categories from {aggregator_category} table.
  155. *
  156. * @return
  157. * An associative array keyed by category ID and values are set to the
  158. * category names.
  159. */
  160. function getCategories() {
  161. $categories = array();
  162. $result = db_query('SELECT * FROM {aggregator_category}');
  163. foreach ($result as $category) {
  164. $categories[$category->cid] = $category;
  165. }
  166. return $categories;
  167. }
  168. /**
  169. * Checks whether the feed name and URL are unique.
  170. *
  171. * @param $feed_name
  172. * String containing the feed name to check.
  173. * @param $feed_url
  174. * String containing the feed URL to check.
  175. *
  176. * @return
  177. * TRUE if feed is unique.
  178. */
  179. function uniqueFeed($feed_name, $feed_url) {
  180. $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed_name, ':url' => $feed_url))->fetchField();
  181. return (1 == $result);
  182. }
  183. /**
  184. * Creates a valid OPML file from an array of feeds.
  185. *
  186. * @param $feeds
  187. * An array of feeds.
  188. *
  189. * @return
  190. * Path to valid OPML file.
  191. */
  192. function getValidOpml($feeds) {
  193. // Properly escape URLs so that XML parsers don't choke on them.
  194. foreach ($feeds as &$feed) {
  195. $feed['url'] = htmlspecialchars($feed['url']);
  196. }
  197. /**
  198. * Does not have an XML declaration, must pass the parser.
  199. */
  200. $opml = <<<EOF
  201. <opml version="1.0">
  202. <head></head>
  203. <body>
  204. <!-- First feed to be imported. -->
  205. <outline text="{$feeds[0]['title']}" xmlurl="{$feeds[0]['url']}" />
  206. <!-- Second feed. Test string delimitation and attribute order. -->
  207. <outline xmlurl='{$feeds[1]['url']}' text='{$feeds[1]['title']}'/>
  208. <!-- Test for duplicate URL and title. -->
  209. <outline xmlurl="{$feeds[0]['url']}" text="Duplicate URL"/>
  210. <outline xmlurl="http://duplicate.title" text="{$feeds[1]['title']}"/>
  211. <!-- Test that feeds are only added with required attributes. -->
  212. <outline text="{$feeds[2]['title']}" />
  213. <outline xmlurl="{$feeds[2]['url']}" />
  214. </body>
  215. </opml>
  216. EOF;
  217. $path = 'public://valid-opml.xml';
  218. return file_unmanaged_save_data($opml, $path);
  219. }
  220. /**
  221. * Creates an invalid OPML file.
  222. *
  223. * @return
  224. * Path to invalid OPML file.
  225. */
  226. function getInvalidOpml() {
  227. $opml = <<<EOF
  228. <opml>
  229. <invalid>
  230. </opml>
  231. EOF;
  232. $path = 'public://invalid-opml.xml';
  233. return file_unmanaged_save_data($opml, $path);
  234. }
  235. /**
  236. * Creates a valid but empty OPML file.
  237. *
  238. * @return
  239. * Path to empty OPML file.
  240. */
  241. function getEmptyOpml() {
  242. $opml = <<<EOF
  243. <?xml version="1.0" encoding="utf-8"?>
  244. <opml version="1.0">
  245. <head></head>
  246. <body>
  247. <outline text="Sample text" />
  248. <outline text="Sample text" url="Sample URL" />
  249. </body>
  250. </opml>
  251. EOF;
  252. $path = 'public://empty-opml.xml';
  253. return file_unmanaged_save_data($opml, $path);
  254. }
  255. function getRSS091Sample() {
  256. return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml';
  257. }
  258. function getAtomSample() {
  259. // The content of this sample ATOM feed is based directly off of the
  260. // example provided in RFC 4287.
  261. return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_atom.xml';
  262. }
  263. function getHtmlEntitiesSample() {
  264. return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_title_entities.xml';
  265. }
  266. function getNoTitleSample() {
  267. return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_title_from_desc.xml';
  268. }
  269. /**
  270. * Creates sample article nodes.
  271. *
  272. * @param $count
  273. * (optional) The number of nodes to generate. Defaults to five.
  274. */
  275. function createSampleNodes($count = 5) {
  276. $langcode = LANGUAGE_NONE;
  277. // Post $count article nodes.
  278. for ($i = 0; $i < $count; $i++) {
  279. $edit = array();
  280. $edit['title'] = $this->randomName();
  281. $edit["body[$langcode][0][value]"] = $this->randomName();
  282. $this->drupalPost('node/add/article', $edit, t('Save'));
  283. }
  284. }
  285. }
  286. /**
  287. * Tests functionality of the configuration settings in the Aggregator module.
  288. */
  289. class AggregatorConfigurationTestCase extends AggregatorTestCase {
  290. public static function getInfo() {
  291. return array(
  292. 'name' => 'Aggregator configuration',
  293. 'description' => 'Test aggregator settings page.',
  294. 'group' => 'Aggregator',
  295. );
  296. }
  297. /**
  298. * Tests the settings form to ensure the correct default values are used.
  299. */
  300. function testSettingsPage() {
  301. $edit = array(
  302. 'aggregator_allowed_html_tags' => '<a>',
  303. 'aggregator_summary_items' => 10,
  304. 'aggregator_clear' => 3600,
  305. 'aggregator_category_selector' => 'select',
  306. 'aggregator_teaser_length' => 200,
  307. );
  308. $this->drupalPost('admin/config/services/aggregator/settings', $edit, t('Save configuration'));
  309. $this->assertText(t('The configuration options have been saved.'));
  310. foreach ($edit as $name => $value) {
  311. $this->assertFieldByName($name, $value, format_string('"@name" has correct default value.', array('@name' => $name)));
  312. }
  313. }
  314. }
  315. /**
  316. * Tests adding aggregator feeds.
  317. */
  318. class AddFeedTestCase extends AggregatorTestCase {
  319. public static function getInfo() {
  320. return array(
  321. 'name' => 'Add feed functionality',
  322. 'description' => 'Add feed test.',
  323. 'group' => 'Aggregator'
  324. );
  325. }
  326. /**
  327. * Creates and ensures that a feed is unique, checks source, and deletes feed.
  328. */
  329. function testAddFeed() {
  330. $feed = $this->createFeed();
  331. // Check feed data.
  332. $this->assertEqual($this->getUrl(), url('admin/config/services/aggregator/add/feed', array('absolute' => TRUE)), 'Directed to correct url.');
  333. $this->assertTrue($this->uniqueFeed($feed->title, $feed->url), 'The feed is unique.');
  334. // Check feed source.
  335. $this->drupalGet('aggregator/sources/' . $feed->fid);
  336. $this->assertResponse(200, 'Feed source exists.');
  337. $this->assertText($feed->title, 'Page title');
  338. $this->drupalGet('aggregator/sources/' . $feed->fid . '/categorize');
  339. $this->assertResponse(200, 'Feed categorization page exists.');
  340. // Delete feed.
  341. $this->deleteFeed($feed);
  342. }
  343. /**
  344. * Tests feeds with very long URLs.
  345. */
  346. function testAddLongFeed() {
  347. // Create a feed with a URL of > 255 characters.
  348. $long_url = "https://www.google.com/search?ix=heb&sourceid=chrome&ie=UTF-8&q=angie+byron#sclient=psy-ab&hl=en&safe=off&source=hp&q=angie+byron&pbx=1&oq=angie+byron&aq=f&aqi=&aql=&gs_sm=3&gs_upl=0l0l0l10534l0l0l0l0l0l0l0l0ll0l0&bav=on.2,or.r_gc.r_pw.r_cp.,cf.osb&fp=a70b6b1f0abe28d8&biw=1629&bih=889&ix=heb";
  349. $feed = $this->createFeed($long_url);
  350. // Create a second feed of > 255 characters, where the only difference is
  351. // after the 255th character.
  352. $long_url_2 = "https://www.google.com/search?ix=heb&sourceid=chrome&ie=UTF-8&q=angie+byron#sclient=psy-ab&hl=en&safe=off&source=hp&q=angie+byron&pbx=1&oq=angie+byron&aq=f&aqi=&aql=&gs_sm=3&gs_upl=0l0l0l10534l0l0l0l0l0l0l0l0ll0l0&bav=on.2,or.r_gc.r_pw.r_cp.,cf.osb&fp=a70b6b1f0abe28d8&biw=1629&bih=889";
  353. $feed_2 = $this->createFeed($long_url_2);
  354. // Check feed data.
  355. $this->assertTrue($this->uniqueFeed($feed->title, $feed->url), 'The first long URL feed is unique.');
  356. $this->assertTrue($this->uniqueFeed($feed_2->title, $feed_2->url), 'The second long URL feed is unique.');
  357. // Check feed source.
  358. $this->drupalGet('aggregator/sources/' . $feed->fid);
  359. $this->assertResponse(200, 'Long URL feed source exists.');
  360. $this->assertText($feed->title, 'Page title');
  361. $this->drupalGet('aggregator/sources/' . $feed->fid . '/categorize');
  362. $this->assertResponse(200, 'Long URL feed categorization page exists.');
  363. // Delete feeds.
  364. $this->deleteFeed($feed);
  365. $this->deleteFeed($feed_2);
  366. }
  367. }
  368. /**
  369. * Tests the categorize feed functionality in the Aggregator module.
  370. */
  371. class CategorizeFeedTestCase extends AggregatorTestCase {
  372. public static function getInfo() {
  373. return array(
  374. 'name' => 'Categorize feed functionality',
  375. 'description' => 'Categorize feed test.',
  376. 'group' => 'Aggregator'
  377. );
  378. }
  379. /**
  380. * Creates a feed and makes sure you can add/delete categories to it.
  381. */
  382. function testCategorizeFeed() {
  383. // Create 2 categories.
  384. $category_1 = array('title' => $this->randomName(10), 'description' => '');
  385. $this->drupalPost('admin/config/services/aggregator/add/category', $category_1, t('Save'));
  386. $this->assertRaw(t('The category %title has been added.', array('%title' => $category_1['title'])), format_string('The category %title has been added.', array('%title' => $category_1['title'])));
  387. $category_2 = array('title' => $this->randomName(10), 'description' => '');
  388. $this->drupalPost('admin/config/services/aggregator/add/category', $category_2, t('Save'));
  389. $this->assertRaw(t('The category %title has been added.', array('%title' => $category_2['title'])), format_string('The category %title has been added.', array('%title' => $category_2['title'])));
  390. // Get categories from database.
  391. $categories = $this->getCategories();
  392. // Create a feed and assign 2 categories to it.
  393. $feed = $this->getFeedEditArray();
  394. $feed['block'] = 5;
  395. foreach ($categories as $cid => $category) {
  396. $feed['category'][$cid] = $cid;
  397. }
  398. // Use aggregator_save_feed() function to save the feed.
  399. aggregator_save_feed($feed);
  400. $db_feed = db_query("SELECT * FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed['title'], ':url' => $feed['url']))->fetch();
  401. // Assert the feed has two categories.
  402. $this->getFeedCategories($db_feed);
  403. $this->assertEqual(count($db_feed->categories), 2, 'Feed has 2 categories');
  404. // Use aggregator_save_feed() to delete a category.
  405. $category = reset($categories);
  406. aggregator_save_category(array('cid' => $category->cid));
  407. // Assert that category is deleted.
  408. $db_category = db_query("SELECT COUNT(*) FROM {aggregator_category} WHERE cid = :cid", array(':cid' => $category->cid))->fetchField();
  409. $this->assertFalse($db_category, format_string('The category %title has been deleted.', array('%title' => $category->title)));
  410. // Assert that category has been removed from feed.
  411. $categorized_feeds = db_query("SELECT COUNT(*) FROM {aggregator_category_feed} WHERE cid = :cid", array(':cid' => $category->cid))->fetchField();
  412. $this->assertFalse($categorized_feeds, format_string('The category %title has been removed from feed %feed_title.', array('%title' => $category->title, '%feed_title' => $feed['title'])));
  413. // Assert that no broken links (associated with the deleted category)
  414. // appear on one of the other category pages.
  415. $this->createSampleNodes();
  416. $this->drupalGet('admin/config/services/aggregator');
  417. $this->clickLink('update items');
  418. $categories = $this->getCategories();
  419. $category = reset($categories);
  420. $this->drupalGet('aggregator/categories/' . $category->cid);
  421. global $base_path;
  422. $this->assertNoRaw('<a href="' . $base_path . 'aggregator/categories/"></a>,');
  423. }
  424. }
  425. /**
  426. * Tests functionality of updating the feed in the Aggregator module.
  427. */
  428. class UpdateFeedTestCase extends AggregatorTestCase {
  429. public static function getInfo() {
  430. return array(
  431. 'name' => 'Update feed functionality',
  432. 'description' => 'Update feed test.',
  433. 'group' => 'Aggregator'
  434. );
  435. }
  436. /**
  437. * Creates a feed and attempts to update it.
  438. */
  439. function testUpdateFeed() {
  440. $remamining_fields = array('title', 'url', '');
  441. foreach ($remamining_fields as $same_field) {
  442. $feed = $this->createFeed();
  443. // Get new feed data array and modify newly created feed.
  444. $edit = $this->getFeedEditArray();
  445. $edit['refresh'] = 1800; // Change refresh value.
  446. if (isset($feed->{$same_field})) {
  447. $edit[$same_field] = $feed->{$same_field};
  448. }
  449. $this->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, $edit, t('Save'));
  450. $this->assertRaw(t('The feed %name has been updated.', array('%name' => $edit['title'])), format_string('The feed %name has been updated.', array('%name' => $edit['title'])));
  451. // Check feed data.
  452. $this->assertEqual($this->getUrl(), url('admin/config/services/aggregator/', array('absolute' => TRUE)));
  453. $this->assertTrue($this->uniqueFeed($edit['title'], $edit['url']), 'The feed is unique.');
  454. // Check feed source.
  455. $this->drupalGet('aggregator/sources/' . $feed->fid);
  456. $this->assertResponse(200, 'Feed source exists.');
  457. $this->assertText($edit['title'], 'Page title');
  458. // Delete feed.
  459. $feed->title = $edit['title']; // Set correct title so deleteFeed() will work.
  460. $this->deleteFeed($feed);
  461. }
  462. }
  463. }
  464. /**
  465. * Tests functionality for removing feeds in the Aggregator module.
  466. */
  467. class RemoveFeedTestCase extends AggregatorTestCase {
  468. public static function getInfo() {
  469. return array(
  470. 'name' => 'Remove feed functionality',
  471. 'description' => 'Remove feed test.',
  472. 'group' => 'Aggregator'
  473. );
  474. }
  475. /**
  476. * Removes a feed and ensures that all of its services are removed.
  477. */
  478. function testRemoveFeed() {
  479. $feed = $this->createFeed();
  480. // Delete feed.
  481. $this->deleteFeed($feed);
  482. // Check feed source.
  483. $this->drupalGet('aggregator/sources/' . $feed->fid);
  484. $this->assertResponse(404, 'Deleted feed source does not exists.');
  485. // Check database for feed.
  486. $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(':title' => $feed->title, ':url' => $feed->url))->fetchField();
  487. $this->assertFalse($result, 'Feed not found in database');
  488. }
  489. }
  490. /**
  491. * Tests functionality of updating a feed item in the Aggregator module.
  492. */
  493. class UpdateFeedItemTestCase extends AggregatorTestCase {
  494. public static function getInfo() {
  495. return array(
  496. 'name' => 'Update feed item functionality',
  497. 'description' => 'Update feed items from a feed.',
  498. 'group' => 'Aggregator'
  499. );
  500. }
  501. /**
  502. * Tests running "update items" from 'admin/config/services/aggregator' page.
  503. */
  504. function testUpdateFeedItem() {
  505. $this->createSampleNodes();
  506. // Create a feed and test updating feed items if possible.
  507. $feed = $this->createFeed();
  508. if (!empty($feed)) {
  509. $this->updateFeedItems($feed, $this->getDefaultFeedItemCount());
  510. $this->removeFeedItems($feed);
  511. }
  512. // Delete feed.
  513. $this->deleteFeed($feed);
  514. // Test updating feed items without valid timestamp information.
  515. $edit = array(
  516. 'title' => "Feed without publish timestamp",
  517. 'url' => $this->getRSS091Sample(),
  518. );
  519. $this->drupalGet($edit['url']);
  520. $this->assertResponse(array(200), format_string('URL !url is accessible', array('!url' => $edit['url'])));
  521. $this->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save'));
  522. $this->assertRaw(t('The feed %name has been added.', array('%name' => $edit['title'])), format_string('The feed !name has been added.', array('!name' => $edit['title'])));
  523. $feed = db_query("SELECT * FROM {aggregator_feed} WHERE url = :url", array(':url' => $edit['url']))->fetchObject();
  524. aggregator_refresh($feed);
  525. $before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField();
  526. // Sleep for 3 second.
  527. sleep(3);
  528. db_update('aggregator_feed')
  529. ->condition('fid', $feed->fid)
  530. ->fields(array(
  531. 'checked' => 0,
  532. 'hash' => '',
  533. 'etag' => '',
  534. 'modified' => 0,
  535. ))
  536. ->execute();
  537. aggregator_refresh($feed);
  538. $after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField();
  539. $this->assertTrue($before === $after, format_string('Publish timestamp of feed item was not updated (!before === !after)', array('!before' => $before, '!after' => $after)));
  540. }
  541. }
  542. class RemoveFeedItemTestCase extends AggregatorTestCase {
  543. public static function getInfo() {
  544. return array(
  545. 'name' => 'Remove feed item functionality',
  546. 'description' => 'Remove feed items from a feed.',
  547. 'group' => 'Aggregator'
  548. );
  549. }
  550. /**
  551. * Tests running "remove items" from 'admin/config/services/aggregator' page.
  552. */
  553. function testRemoveFeedItem() {
  554. // Create a bunch of test feeds.
  555. $feed_urls = array();
  556. // No last-modified, no etag.
  557. $feed_urls[] = url('aggregator/test-feed', array('absolute' => TRUE));
  558. // Last-modified, but no etag.
  559. $feed_urls[] = url('aggregator/test-feed/1', array('absolute' => TRUE));
  560. // No Last-modified, but etag.
  561. $feed_urls[] = url('aggregator/test-feed/0/1', array('absolute' => TRUE));
  562. // Last-modified and etag.
  563. $feed_urls[] = url('aggregator/test-feed/1/1', array('absolute' => TRUE));
  564. foreach ($feed_urls as $feed_url) {
  565. $feed = $this->createFeed($feed_url);
  566. // Update and remove items two times in a row to make sure that removal
  567. // resets all 'modified' information (modified, etag, hash) and allows for
  568. // immediate update.
  569. $this->updateAndRemove($feed, 4);
  570. $this->updateAndRemove($feed, 4);
  571. $this->updateAndRemove($feed, 4);
  572. // Delete feed.
  573. $this->deleteFeed($feed);
  574. }
  575. }
  576. }
  577. /**
  578. * Tests categorization functionality in the Aggregator module.
  579. */
  580. class CategorizeFeedItemTestCase extends AggregatorTestCase {
  581. public static function getInfo() {
  582. return array(
  583. 'name' => 'Categorize feed item functionality',
  584. 'description' => 'Test feed item categorization.',
  585. 'group' => 'Aggregator'
  586. );
  587. }
  588. /**
  589. * Checks that children of a feed inherit a defined category.
  590. *
  591. * If a feed has a category, make sure that the children inherit that
  592. * categorization.
  593. */
  594. function testCategorizeFeedItem() {
  595. $this->createSampleNodes();
  596. // Simulate form submission on "admin/config/services/aggregator/add/category".
  597. $edit = array('title' => $this->randomName(10), 'description' => '');
  598. $this->drupalPost('admin/config/services/aggregator/add/category', $edit, t('Save'));
  599. $this->assertRaw(t('The category %title has been added.', array('%title' => $edit['title'])), format_string('The category %title has been added.', array('%title' => $edit['title'])));
  600. $category = db_query("SELECT * FROM {aggregator_category} WHERE title = :title", array(':title' => $edit['title']))->fetch();
  601. $this->assertTrue(!empty($category), 'The category found in database.');
  602. $link_path = 'aggregator/categories/' . $category->cid;
  603. $menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $link_path))->fetch();
  604. $this->assertTrue(!empty($menu_link), 'The menu link associated with the category found in database.');
  605. $feed = $this->createFeed();
  606. db_insert('aggregator_category_feed')
  607. ->fields(array(
  608. 'cid' => $category->cid,
  609. 'fid' => $feed->fid,
  610. ))
  611. ->execute();
  612. $this->updateFeedItems($feed, $this->getDefaultFeedItemCount());
  613. $this->getFeedCategories($feed);
  614. $this->assertTrue(!empty($feed->categories), 'The category found in the feed.');
  615. // For each category of a feed, ensure feed items have that category, too.
  616. if (!empty($feed->categories) && !empty($feed->items)) {
  617. foreach ($feed->categories as $category) {
  618. $categorized_count = db_select('aggregator_category_item')
  619. ->condition('iid', $feed->items, 'IN')
  620. ->countQuery()
  621. ->execute()
  622. ->fetchField();
  623. $this->assertEqual($feed->item_count, $categorized_count, 'Total items in feed equal to the total categorized feed items in database');
  624. }
  625. }
  626. // Delete category from feed items when category is deleted.
  627. $cid = reset($feed->categories);
  628. $categories = $this->getCategories();
  629. $category_title = $categories[$cid]->title;
  630. // Delete category.
  631. aggregator_save_category(array('cid' => $cid));
  632. // Assert category has been removed from feed items.
  633. $categorized_count = db_query("SELECT COUNT(*) FROM {aggregator_category_item} WHERE cid = :cid", array(':cid' => $cid))->fetchField();
  634. $this->assertFalse($categorized_count, format_string('The category %title has been removed from feed items.', array('%title' => $category_title)));
  635. // Delete feed.
  636. $this->deleteFeed($feed);
  637. }
  638. }
  639. /**
  640. * Tests importing feeds from OPML functionality for the Aggregator module.
  641. */
  642. class ImportOPMLTestCase extends AggregatorTestCase {
  643. public static function getInfo() {
  644. return array(
  645. 'name' => 'Import feeds from OPML functionality',
  646. 'description' => 'Test OPML import.',
  647. 'group' => 'Aggregator',
  648. );
  649. }
  650. /**
  651. * Opens OPML import form.
  652. */
  653. function openImportForm() {
  654. db_delete('aggregator_category')->execute();
  655. $category = $this->randomName(10);
  656. $cid = db_insert('aggregator_category')
  657. ->fields(array(
  658. 'title' => $category,
  659. 'description' => '',
  660. ))
  661. ->execute();
  662. $this->drupalGet('admin/config/services/aggregator/add/opml');
  663. $this->assertText('A single OPML document may contain a collection of many feeds.', 'Found OPML help text.');
  664. $this->assertField('files[upload]', 'Found file upload field.');
  665. $this->assertField('remote', 'Found Remote URL field.');
  666. $this->assertField('refresh', 'Found Refresh field.');
  667. $this->assertFieldByName("category[$cid]", $cid, 'Found category field.');
  668. }
  669. /**
  670. * Submits form filled with invalid fields.
  671. */
  672. function validateImportFormFields() {
  673. $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
  674. $edit = array();
  675. $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
  676. $this->assertRaw(t('You must <em>either</em> upload a file or enter a URL.'), 'Error if no fields are filled.');
  677. $path = $this->getEmptyOpml();
  678. $edit = array(
  679. 'files[upload]' => $path,
  680. 'remote' => file_create_url($path),
  681. );
  682. $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
  683. $this->assertRaw(t('You must <em>either</em> upload a file or enter a URL.'), 'Error if both fields are filled.');
  684. $edit = array('remote' => 'invalidUrl://empty');
  685. $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
  686. $this->assertText(t('This URL is not valid.'), 'Error if the URL is invalid.');
  687. $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
  688. $this->assertEqual($before, $after, 'No feeds were added during the three last form submissions.');
  689. }
  690. /**
  691. * Submits form with invalid, empty, and valid OPML files.
  692. */
  693. function submitImportForm() {
  694. $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
  695. $form['files[upload]'] = $this->getInvalidOpml();
  696. $this->drupalPost('admin/config/services/aggregator/add/opml', $form, t('Import'));
  697. $this->assertText(t('No new feed has been added.'), 'Attempting to upload invalid XML.');
  698. $edit = array('remote' => file_create_url($this->getEmptyOpml()));
  699. $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
  700. $this->assertText(t('No new feed has been added.'), 'Attempting to load empty OPML from remote URL.');
  701. $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
  702. $this->assertEqual($before, $after, 'No feeds were added during the two last form submissions.');
  703. db_delete('aggregator_feed')->execute();
  704. db_delete('aggregator_category')->execute();
  705. db_delete('aggregator_category_feed')->execute();
  706. $category = $this->randomName(10);
  707. db_insert('aggregator_category')
  708. ->fields(array(
  709. 'cid' => 1,
  710. 'title' => $category,
  711. 'description' => '',
  712. ))
  713. ->execute();
  714. $feeds[0] = $this->getFeedEditArray();
  715. $feeds[1] = $this->getFeedEditArray();
  716. $feeds[2] = $this->getFeedEditArray();
  717. $edit = array(
  718. 'files[upload]' => $this->getValidOpml($feeds),
  719. 'refresh' => '900',
  720. 'category[1]' => $category,
  721. );
  722. $this->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
  723. $this->assertRaw(t('A feed with the URL %url already exists.', array('%url' => $feeds[0]['url'])), 'Verifying that a duplicate URL was identified');
  724. $this->assertRaw(t('A feed named %title already exists.', array('%title' => $feeds[1]['title'])), 'Verifying that a duplicate title was identified');
  725. $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')->fetchField();
  726. $this->assertEqual($after, 2, 'Verifying that two distinct feeds were added.');
  727. $feeds_from_db = db_query("SELECT f.title, f.url, f.refresh, cf.cid FROM {aggregator_feed} f LEFT JOIN {aggregator_category_feed} cf ON f.fid = cf.fid");
  728. $refresh = $category = TRUE;
  729. foreach ($feeds_from_db as $feed) {
  730. $title[$feed->url] = $feed->title;
  731. $url[$feed->title] = $feed->url;
  732. $category = $category && $feed->cid == 1;
  733. $refresh = $refresh && $feed->refresh == 900;
  734. }
  735. $this->assertEqual($title[$feeds[0]['url']], $feeds[0]['title'], 'First feed was added correctly.');
  736. $this->assertEqual($url[$feeds[1]['title']], $feeds[1]['url'], 'Second feed was added correctly.');
  737. $this->assertTrue($refresh, 'Refresh times are correct.');
  738. $this->assertTrue($category, 'Categories are correct.');
  739. }
  740. /**
  741. * Tests the import of an OPML file.
  742. */
  743. function testOPMLImport() {
  744. $this->openImportForm();
  745. $this->validateImportFormFields();
  746. $this->submitImportForm();
  747. }
  748. }
  749. /**
  750. * Tests functionality of the cron process in the Aggregator module.
  751. */
  752. class AggregatorCronTestCase extends AggregatorTestCase {
  753. public static function getInfo() {
  754. return array(
  755. 'name' => 'Update on cron functionality',
  756. 'description' => 'Update feeds on cron.',
  757. 'group' => 'Aggregator'
  758. );
  759. }
  760. /**
  761. * Adds feeds and updates them via cron process.
  762. */
  763. public function testCron() {
  764. // Create feed and test basic updating on cron.
  765. $this->createSampleNodes();
  766. $feed = $this->createFeed();
  767. $this->cronRun();
  768. $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.');
  769. $this->removeFeedItems($feed);
  770. $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.');
  771. $this->cronRun();
  772. $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.');
  773. // Test feed locking when queued for update.
  774. $this->removeFeedItems($feed);
  775. db_update('aggregator_feed')
  776. ->condition('fid', $feed->fid)
  777. ->fields(array(
  778. 'queued' => REQUEST_TIME,
  779. ))
  780. ->execute();
  781. $this->cronRun();
  782. $this->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.');
  783. db_update('aggregator_feed')
  784. ->condition('fid', $feed->fid)
  785. ->fields(array(
  786. 'queued' => 0,
  787. ))
  788. ->execute();
  789. $this->cronRun();
  790. $this->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchField(), 'Expected number of items in database.');
  791. }
  792. }
  793. /**
  794. * Tests rendering functionality in the Aggregator module.
  795. */
  796. class AggregatorRenderingTestCase extends AggregatorTestCase {
  797. protected $admin_user;
  798. public static function getInfo() {
  799. return array(
  800. 'name' => 'Checks display of aggregator items',
  801. 'description' => 'Checks display of aggregator items on the page.',
  802. 'group' => 'Aggregator'
  803. );
  804. }
  805. /**
  806. * Adds a feed block to the page and checks its links.
  807. *
  808. * @todo Test the category block as well.
  809. */
  810. public function testBlockLinks() {
  811. // Create feed.
  812. $this->createSampleNodes();
  813. $feed = $this->createFeed();
  814. $this->updateFeedItems($feed, $this->getDefaultFeedItemCount());
  815. // Place block on page (@see block.test:moveBlockToRegion())
  816. // Need admin user to be able to access block admin.
  817. $this->admin_user = $this->drupalCreateUser(array(
  818. 'administer blocks',
  819. 'access administration pages',
  820. 'administer news feeds',
  821. 'access news feeds',
  822. ));
  823. $this->drupalLogin($this->admin_user);
  824. // Prepare to use the block admin form.
  825. $block = array(
  826. 'module' => 'aggregator',
  827. 'delta' => 'feed-' . $feed->fid,
  828. 'title' => $feed->title,
  829. );
  830. $region = 'footer';
  831. $edit = array();
  832. $edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = $region;
  833. // Check the feed block is available in the block list form.
  834. $this->drupalGet('admin/structure/block');
  835. $this->assertFieldByName('blocks[' . $block['module'] . '_' . $block['delta'] . '][region]', '', 'Aggregator feed block is available for positioning.');
  836. // Position it.
  837. $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
  838. $this->assertText(t('The block settings have been updated.'), format_string('Block successfully moved to %region_name region.', array( '%region_name' => $region)));
  839. // Confirm that the block is now being displayed on pages.
  840. $this->drupalGet('node');
  841. $this->assertText(t($block['title']), 'Feed block is displayed on the page.');
  842. // Find the expected read_more link.
  843. $href = 'aggregator/sources/' . $feed->fid;
  844. $links = $this->xpath('//a[@href = :href]', array(':href' => url($href)));
  845. $this->assert(isset($links[0]), format_string('Link to href %href found.', array('%href' => $href)));
  846. // Visit that page.
  847. $this->drupalGet($href);
  848. $correct_titles = $this->xpath('//h1[normalize-space(text())=:title]', array(':title' => $feed->title));
  849. $this->assertFalse(empty($correct_titles), 'Aggregator feed page is available and has the correct title.');
  850. // Set the number of news items to 0 to test that the block does not show
  851. // up.
  852. $feed->block = 0;
  853. aggregator_save_feed((array) $feed);
  854. // It is necessary to flush the cache after saving the number of items.
  855. drupal_flush_all_caches();
  856. // Check that the block is no longer displayed.
  857. $this->drupalGet('node');
  858. $this->assertNoText(t($block['title']), 'Feed block is not displayed on the page when number of items is set to 0.');
  859. }
  860. /**
  861. * Creates a feed and checks that feed's page.
  862. */
  863. public function testFeedPage() {
  864. // Increase the number of items published in the rss.xml feed so we have
  865. // enough articles to test paging.
  866. variable_set('feed_default_items', 30);
  867. // Create a feed with 30 items.
  868. $this->createSampleNodes(30);
  869. $feed = $this->createFeed();
  870. $this->updateFeedItems($feed, 30);
  871. // Check for the presence of a pager.
  872. $this->drupalGet('aggregator/sources/' . $feed->fid);
  873. $elements = $this->xpath("//ul[@class=:class]", array(':class' => 'pager'));
  874. $this->assertTrue(!empty($elements), 'Individual source page contains a pager.');
  875. // Reset the number of items in rss.xml to the default value.
  876. variable_set('feed_default_items', 10);
  877. }
  878. }
  879. /**
  880. * Tests feed parsing in the Aggregator module.
  881. */
  882. class FeedParserTestCase extends AggregatorTestCase {
  883. public static function getInfo() {
  884. return array(
  885. 'name' => 'Feed parser functionality',
  886. 'description' => 'Test the built-in feed parser with valid feed samples.',
  887. 'group' => 'Aggregator',
  888. );
  889. }
  890. function setUp() {
  891. parent::setUp();
  892. // Do not remove old aggregator items during these tests, since our sample
  893. // feeds have hardcoded dates in them (which may be expired when this test
  894. // is run).
  895. variable_set('aggregator_clear', AGGREGATOR_CLEAR_NEVER);
  896. }
  897. /**
  898. * Tests a feed that uses the RSS 0.91 format.
  899. */
  900. function testRSS091Sample() {
  901. $feed = $this->createFeed($this->getRSS091Sample());
  902. aggregator_refresh($feed);
  903. $this->drupalGet('aggregator/sources/' . $feed->fid);
  904. $this->assertResponse(200, format_string('Feed %name exists.', array('%name' => $feed->title)));
  905. $this->assertText('First example feed item title');
  906. $this->assertLinkByHref('http://example.com/example-turns-one');
  907. $this->assertText('First example feed item description.');
  908. // Several additional items that include elements over 255 characters.
  909. $this->assertRaw("Second example feed item title.");
  910. $this->assertText('Long link feed item title');
  911. $this->assertText('Long link feed item description');
  912. $this->assertLinkByHref('http://example.com/tomorrow/and/tomorrow/and/tomorrow/creeps/in/this/petty/pace/from/day/to/day/to/the/last/syllable/of/recorded/time/and/all/our/yesterdays/have/lighted/fools/the/way/to/dusty/death/out/out/brief/candle/life/is/but/a/walking/shadow/a/poor/player/that/struts/and/frets/his/hour/upon/the/stage/and/is/heard/no/more/it/is/a/tale/told/by/an/idiot/full/of/sound/and/fury/signifying/nothing');
  913. $this->assertText('Long author feed item title');
  914. $this->assertText('Long author feed item description');
  915. $this->assertLinkByHref('http://example.com/long/author');
  916. }
  917. /**
  918. * Tests a feed that uses the Atom format.
  919. */
  920. function testAtomSample() {
  921. $feed = $this->createFeed($this->getAtomSample());
  922. aggregator_refresh($feed);
  923. $this->drupalGet('aggregator/sources/' . $feed->fid);
  924. $this->assertResponse(200, format_string('Feed %name exists.', array('%name' => $feed->title)));
  925. $this->assertText('Atom-Powered Robots Run Amok');
  926. $this->assertLinkByHref('http://example.org/2003/12/13/atom03');
  927. $this->assertText('Some text.');
  928. $this->assertEqual('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', db_query('SELECT guid FROM {aggregator_item} WHERE link = :link', array(':link' => 'http://example.org/2003/12/13/atom03'))->fetchField(), 'Atom entry id element is parsed correctly.');
  929. }
  930. /**
  931. * Tests a feed that uses HTML entities in item titles.
  932. */
  933. function testHtmlEntitiesSample() {
  934. $feed = $this->createFeed($this->getHtmlEntitiesSample());
  935. aggregator_refresh($feed);
  936. $this->drupalGet('aggregator/sources/' . $feed->fid);
  937. $this->assertResponse(200, format_string('Feed %name exists.', array('%name' => $feed->title)));
  938. $this->assertRaw("Quote&quot; Amp&amp;");
  939. }
  940. /**
  941. * Tests a feed with no title; the description should be used to generate one.
  942. */
  943. function testNoTitleSample() {
  944. $feed = $this->createFeed($this->getNoTitleSample());
  945. aggregator_refresh($feed);
  946. $this->drupalGet('aggregator/sources/' . $feed->fid);
  947. $this->assertResponse(200, format_string('Feed %name exists.', array('%name' => $feed->title)));
  948. $this->assertRaw("This description should be used to generate a title. This description should be used to generate a title. This description should be used to generate a title. This description should be used to generate a title. This description should be used to...");
  949. }
  950. }