aggregator.test 36 KB

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