aggregator.test

Tests for aggregator.module.

File

drupal/modules/aggregator/aggregator.test
View source
<?php

/**
 * @file
 * Tests for aggregator.module.
 */

/**
 * Defines a base class for testing the Aggregator module.
 */
class AggregatorTestCase extends DrupalWebTestCase {
  function setUp() {
    parent::setUp('aggregator', 'aggregator_test');
    $web_user = $this
      ->drupalCreateUser(array(
      'administer news feeds',
      'access news feeds',
      'create article content',
    ));
    $this
      ->drupalLogin($web_user);
  }

  /**
   * Creates an aggregator feed.
   *
   * This method simulates the form submission on path
   * admin/config/services/aggregator/add/feed.
   *
   * @param $feed_url
   *   (optional) If given, feed will be created with this URL, otherwise
   *   /rss.xml will be used. Defaults to NULL.
   *
   * @return $feed
   *   Full feed object if possible.
   *
   * @see getFeedEditArray()
   */
  function createFeed($feed_url = NULL) {
    $edit = $this
      ->getFeedEditArray($feed_url);
    $this
      ->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save'));
    $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'],
    )));
    $feed = db_query("SELECT *  FROM {aggregator_feed} WHERE title = :title AND url = :url", array(
      ':title' => $edit['title'],
      ':url' => $edit['url'],
    ))
      ->fetch();
    $this
      ->assertTrue(!empty($feed), 'The feed found in database.');
    return $feed;
  }

  /**
   * Deletes an aggregator feed.
   *
   * @param $feed
   *   Feed object representing the feed.
   */
  function deleteFeed($feed) {
    $this
      ->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, array(), t('Delete'));
    $this
      ->assertRaw(t('The feed %title has been deleted.', array(
      '%title' => $feed->title,
    )), 'Feed deleted successfully.');
  }

  /**
   * Returns a randomly generated feed edit array.
   *
   * @param $feed_url
   *   (optional) If given, feed will be created with this URL, otherwise
   *   /rss.xml will be used. Defaults to NULL.
   * @return
   *   A feed array.
   */
  function getFeedEditArray($feed_url = NULL) {
    $feed_name = $this
      ->randomName(10);
    if (!$feed_url) {
      $feed_url = url('rss.xml', array(
        'query' => array(
          'feed' => $feed_name,
        ),
        'absolute' => TRUE,
      ));
    }
    $edit = array(
      'title' => $feed_name,
      'url' => $feed_url,
      'refresh' => '900',
    );
    return $edit;
  }

  /**
   * Returns the count of the randomly created feed array.
   *
   * @return
   *   Number of feed items on default feed created by createFeed().
   */
  function getDefaultFeedItemCount() {

    // Our tests are based off of rss.xml, so let's find out how many elements should be related.
    $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();
    return $feed_count > 10 ? 10 : $feed_count;
  }

  /**
   * Updates the feed items.
   *
   * This method simulates a click to
   * admin/config/services/aggregator/update/$fid.
   *
   * @param $feed
   *   Feed object representing the feed, passed by reference.
   * @param $expected_count
   *   Expected number of feed items.
   */
  function updateFeedItems(&$feed, $expected_count) {

    // First, let's ensure we can get to the rss xml.
    $this
      ->drupalGet($feed->url);
    $this
      ->assertResponse(200, format_string('!url is reachable.', array(
      '!url' => $feed->url,
    )));

    // Attempt to access the update link directly without an access token.
    $this
      ->drupalGet('admin/config/services/aggregator/update/' . $feed->fid);
    $this
      ->assertResponse(403);

    // Refresh the feed (simulated link click).
    $this
      ->drupalGet('admin/config/services/aggregator');
    $this
      ->clickLink('update items');

    // Ensure we have the right number of items.
    $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ));
    $items = array();
    $feed->items = array();
    foreach ($result as $item) {
      $feed->items[] = $item->iid;
    }
    $feed->item_count = count($feed->items);
    $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,
    )));
  }

  /**
   * Confirms an item removal from a feed.
   *
   * @param $feed
   *   Feed object representing the feed.
   */
  function removeFeedItems($feed) {
    $this
      ->drupalPost('admin/config/services/aggregator/remove/' . $feed->fid, array(), t('Remove items'));
    $this
      ->assertRaw(t('The news items from %title have been removed.', array(
      '%title' => $feed->title,
    )), 'Feed items removed.');
  }

  /**
   * Adds and removes feed items and ensure that the count is zero.
   *
   * @param $feed
   *   Feed object representing the feed.
   * @param $expected_count
   *   Expected number of feed items.
   */
  function updateAndRemove($feed, $expected_count) {
    $this
      ->updateFeedItems($feed, $expected_count);
    $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField();
    $this
      ->assertTrue($count);
    $this
      ->removeFeedItems($feed);
    $count = db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField();
    $this
      ->assertTrue($count == 0);
  }

  /**
   * Pulls feed categories from {aggregator_category_feed} table.
   *
   * @param $feed
   *   Feed object representing the feed.
   */
  function getFeedCategories($feed) {

    // add the categories to the feed so we can use them
    $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ));
    foreach ($result as $category) {
      $feed->categories[] = $category->cid;
    }
  }

  /**
   * Pulls categories from {aggregator_category} table.
   *
   * @return
   *   An associative array keyed by category ID and values are set to the
   *   category names.
   */
  function getCategories() {
    $categories = array();
    $result = db_query('SELECT * FROM {aggregator_category}');
    foreach ($result as $category) {
      $categories[$category->cid] = $category;
    }
    return $categories;
  }

  /**
   * Checks whether the feed name and URL are unique.
   *
   * @param $feed_name
   *   String containing the feed name to check.
   * @param $feed_url
   *   String containing the feed URL to check.
   *
   * @return
   *   TRUE if feed is unique.
   */
  function uniqueFeed($feed_name, $feed_url) {
    $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(
      ':title' => $feed_name,
      ':url' => $feed_url,
    ))
      ->fetchField();
    return 1 == $result;
  }

  /**
   * Creates a valid OPML file from an array of feeds.
   *
   * @param $feeds
   *   An array of feeds.
   *
   * @return
   *   Path to valid OPML file.
   */
  function getValidOpml($feeds) {

    // Properly escape URLs so that XML parsers don't choke on them.
    foreach ($feeds as &$feed) {
      $feed['url'] = htmlspecialchars($feed['url']);
    }

    /**
     * Does not have an XML declaration, must pass the parser.
     */
    $opml = <<<EOF
<opml version="1.0">
  <head></head>
  <body>
    <!-- First feed to be imported. -->
    <outline text="{<span class="php-variable">$feeds</span>[<span class="php-constant">0</span>][<span class="php-string">'title'</span>]}" xmlurl="{<span class="php-variable">$feeds</span>[<span class="php-constant">0</span>][<span class="php-string">'url'</span>]}" />

    <!-- Second feed. Test string delimitation and attribute order. -->
    <outline xmlurl='{<span class="php-variable">$feeds</span>[<span class="php-constant">1</span>][<span class="php-string">'url'</span>]}' text='{<span class="php-variable">$feeds</span>[<span class="php-constant">1</span>][<span class="php-string">'title'</span>]}'/>

    <!-- Test for duplicate URL and title. -->
    <outline xmlurl="{<span class="php-variable">$feeds</span>[<span class="php-constant">0</span>][<span class="php-string">'url'</span>]}" text="Duplicate URL"/>
    <outline xmlurl="http://duplicate.title" text="{<span class="php-variable">$feeds</span>[<span class="php-constant">1</span>][<span class="php-string">'title'</span>]}"/>

    <!-- Test that feeds are only added with required attributes. -->
    <outline text="{<span class="php-variable">$feeds</span>[<span class="php-constant">2</span>][<span class="php-string">'title'</span>]}" />
    <outline xmlurl="{<span class="php-variable">$feeds</span>[<span class="php-constant">2</span>][<span class="php-string">'url'</span>]}" />
  </body>
</opml>
EOF;
    $path = 'public://valid-opml.xml';
    return file_unmanaged_save_data($opml, $path);
  }

  /**
   * Creates an invalid OPML file.
   *
   * @return
   *   Path to invalid OPML file.
   */
  function getInvalidOpml() {
    $opml = <<<EOF
<opml>
  <invalid>
</opml>
EOF;
    $path = 'public://invalid-opml.xml';
    return file_unmanaged_save_data($opml, $path);
  }

  /**
   * Creates a valid but empty OPML file.
   *
   * @return
   *   Path to empty OPML file.
   */
  function getEmptyOpml() {
    $opml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
  <head></head>
  <body>
    <outline text="Sample text" />
    <outline text="Sample text" url="Sample URL" />
  </body>
</opml>
EOF;
    $path = 'public://empty-opml.xml';
    return file_unmanaged_save_data($opml, $path);
  }
  function getRSS091Sample() {
    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml';
  }
  function getAtomSample() {

    // The content of this sample ATOM feed is based directly off of the
    // example provided in RFC 4287.
    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_atom.xml';
  }
  function getHtmlEntitiesSample() {
    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_title_entities.xml';
  }

  /**
   * Creates sample article nodes.
   *
   * @param $count
   *   (optional) The number of nodes to generate. Defaults to five.
   */
  function createSampleNodes($count = 5) {
    $langcode = LANGUAGE_NONE;

    // Post $count article nodes.
    for ($i = 0; $i < $count; $i++) {
      $edit = array();
      $edit['title'] = $this
        ->randomName();
      $edit["body[{$langcode}][0][value]"] = $this
        ->randomName();
      $this
        ->drupalPost('node/add/article', $edit, t('Save'));
    }
  }

}

/**
 * Tests functionality of the configuration settings in the Aggregator module.
 */
class AggregatorConfigurationTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Aggregator configuration',
      'description' => 'Test aggregator settings page.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Tests the settings form to ensure the correct default values are used.
   */
  function testSettingsPage() {
    $edit = array(
      'aggregator_allowed_html_tags' => '<a>',
      'aggregator_summary_items' => 10,
      'aggregator_clear' => 3600,
      'aggregator_category_selector' => 'select',
      'aggregator_teaser_length' => 200,
    );
    $this
      ->drupalPost('admin/config/services/aggregator/settings', $edit, t('Save configuration'));
    $this
      ->assertText(t('The configuration options have been saved.'));
    foreach ($edit as $name => $value) {
      $this
        ->assertFieldByName($name, $value, format_string('"@name" has correct default value.', array(
        '@name' => $name,
      )));
    }
  }

}

/**
 * Tests adding aggregator feeds.
 */
class AddFeedTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Add feed functionality',
      'description' => 'Add feed test.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Creates and ensures that a feed is unique, checks source, and deletes feed.
   */
  function testAddFeed() {
    $feed = $this
      ->createFeed();

    // Check feed data.
    $this
      ->assertEqual($this
      ->getUrl(), url('admin/config/services/aggregator/add/feed', array(
      'absolute' => TRUE,
    )), 'Directed to correct url.');
    $this
      ->assertTrue($this
      ->uniqueFeed($feed->title, $feed->url), 'The feed is unique.');

    // Check feed source.
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $this
      ->assertResponse(200, 'Feed source exists.');
    $this
      ->assertText($feed->title, 'Page title');
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid . '/categorize');
    $this
      ->assertResponse(200, 'Feed categorization page exists.');

    // Delete feed.
    $this
      ->deleteFeed($feed);
  }

  /**
   * Tests feeds with very long URLs.
   */
  function testAddLongFeed() {

    // Create a feed with a URL of > 255 characters.
    $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";
    $feed = $this
      ->createFeed($long_url);

    // Create a second feed of > 255 characters, where the only difference is
    // after the 255th character.
    $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";
    $feed_2 = $this
      ->createFeed($long_url_2);

    // Check feed data.
    $this
      ->assertTrue($this
      ->uniqueFeed($feed->title, $feed->url), 'The first long URL feed is unique.');
    $this
      ->assertTrue($this
      ->uniqueFeed($feed_2->title, $feed_2->url), 'The second long URL feed is unique.');

    // Check feed source.
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $this
      ->assertResponse(200, 'Long URL feed source exists.');
    $this
      ->assertText($feed->title, 'Page title');
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid . '/categorize');
    $this
      ->assertResponse(200, 'Long URL feed categorization page exists.');

    // Delete feeds.
    $this
      ->deleteFeed($feed);
    $this
      ->deleteFeed($feed_2);
  }

}

/**
 * Tests the categorize feed functionality in the Aggregator module.
 */
class CategorizeFeedTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Categorize feed functionality',
      'description' => 'Categorize feed test.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Creates a feed and makes sure you can add/delete categories to it.
   */
  function testCategorizeFeed() {

    // Create 2 categories.
    $category_1 = array(
      'title' => $this
        ->randomName(10),
      'description' => '',
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/category', $category_1, t('Save'));
    $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'],
    )));
    $category_2 = array(
      'title' => $this
        ->randomName(10),
      'description' => '',
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/category', $category_2, t('Save'));
    $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'],
    )));

    // Get categories from database.
    $categories = $this
      ->getCategories();

    // Create a feed and assign 2 categories to it.
    $feed = $this
      ->getFeedEditArray();
    $feed['block'] = 5;
    foreach ($categories as $cid => $category) {
      $feed['category'][$cid] = $cid;
    }

    // Use aggregator_save_feed() function to save the feed.
    aggregator_save_feed($feed);
    $db_feed = db_query("SELECT *  FROM {aggregator_feed} WHERE title = :title AND url = :url", array(
      ':title' => $feed['title'],
      ':url' => $feed['url'],
    ))
      ->fetch();

    // Assert the feed has two categories.
    $this
      ->getFeedCategories($db_feed);
    $this
      ->assertEqual(count($db_feed->categories), 2, 'Feed has 2 categories');

    // Use aggregator_save_feed() to delete a category.
    $category = reset($categories);
    aggregator_save_category(array(
      'cid' => $category->cid,
    ));

    // Assert that category is deleted.
    $db_category = db_query("SELECT COUNT(*) FROM {aggregator_category} WHERE cid = :cid", array(
      ':cid' => $category->cid,
    ))
      ->fetchField();
    $this
      ->assertFalse($db_category, format_string('The category %title has been deleted.', array(
      '%title' => $category->title,
    )));

    // Assert that category has been removed from feed.
    $categorized_feeds = db_query("SELECT COUNT(*) FROM {aggregator_category_feed} WHERE cid = :cid", array(
      ':cid' => $category->cid,
    ))
      ->fetchField();
    $this
      ->assertFalse($categorized_feeds, format_string('The category %title has been removed from feed %feed_title.', array(
      '%title' => $category->title,
      '%feed_title' => $feed['title'],
    )));

    // Assert that no broken links (associated with the deleted category)
    // appear on one of the other category pages.
    $this
      ->createSampleNodes();
    $this
      ->drupalGet('admin/config/services/aggregator');
    $this
      ->clickLink('update items');
    $categories = $this
      ->getCategories();
    $category = reset($categories);
    $this
      ->drupalGet('aggregator/categories/' . $category->cid);
    global $base_path;
    $this
      ->assertNoRaw('<a href="' . $base_path . 'aggregator/categories/"></a>,');
  }

}

/**
 * Tests functionality of updating the feed in the Aggregator module.
 */
class UpdateFeedTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Update feed functionality',
      'description' => 'Update feed test.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Creates a feed and attempts to update it.
   */
  function testUpdateFeed() {
    $remamining_fields = array(
      'title',
      'url',
      '',
    );
    foreach ($remamining_fields as $same_field) {
      $feed = $this
        ->createFeed();

      // Get new feed data array and modify newly created feed.
      $edit = $this
        ->getFeedEditArray();
      $edit['refresh'] = 1800;

      // Change refresh value.
      if (isset($feed->{$same_field})) {
        $edit[$same_field] = $feed->{$same_field};
      }
      $this
        ->drupalPost('admin/config/services/aggregator/edit/feed/' . $feed->fid, $edit, t('Save'));
      $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'],
      )));

      // Check feed data.
      $this
        ->assertEqual($this
        ->getUrl(), url('admin/config/services/aggregator/', array(
        'absolute' => TRUE,
      )));
      $this
        ->assertTrue($this
        ->uniqueFeed($edit['title'], $edit['url']), 'The feed is unique.');

      // Check feed source.
      $this
        ->drupalGet('aggregator/sources/' . $feed->fid);
      $this
        ->assertResponse(200, 'Feed source exists.');
      $this
        ->assertText($edit['title'], 'Page title');

      // Delete feed.
      $feed->title = $edit['title'];

      // Set correct title so deleteFeed() will work.
      $this
        ->deleteFeed($feed);
    }
  }

}

/**
 * Tests functionality for removing feeds in the Aggregator module.
 */
class RemoveFeedTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Remove feed functionality',
      'description' => 'Remove feed test.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Removes a feed and ensures that all of its services are removed.
   */
  function testRemoveFeed() {
    $feed = $this
      ->createFeed();

    // Delete feed.
    $this
      ->deleteFeed($feed);

    // Check feed source.
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $this
      ->assertResponse(404, 'Deleted feed source does not exists.');

    // Check database for feed.
    $result = db_query("SELECT COUNT(*) FROM {aggregator_feed} WHERE title = :title AND url = :url", array(
      ':title' => $feed->title,
      ':url' => $feed->url,
    ))
      ->fetchField();
    $this
      ->assertFalse($result, 'Feed not found in database');
  }

}

/**
 * Tests functionality of updating a feed item in the Aggregator module.
 */
class UpdateFeedItemTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Update feed item functionality',
      'description' => 'Update feed items from a feed.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Tests running "update items" from 'admin/config/services/aggregator' page.
   */
  function testUpdateFeedItem() {
    $this
      ->createSampleNodes();

    // Create a feed and test updating feed items if possible.
    $feed = $this
      ->createFeed();
    if (!empty($feed)) {
      $this
        ->updateFeedItems($feed, $this
        ->getDefaultFeedItemCount());
      $this
        ->removeFeedItems($feed);
    }

    // Delete feed.
    $this
      ->deleteFeed($feed);

    // Test updating feed items without valid timestamp information.
    $edit = array(
      'title' => "Feed without publish timestamp",
      'url' => $this
        ->getRSS091Sample(),
    );
    $this
      ->drupalGet($edit['url']);
    $this
      ->assertResponse(array(
      200,
    ), format_string('URL !url is accessible', array(
      '!url' => $edit['url'],
    )));
    $this
      ->drupalPost('admin/config/services/aggregator/add/feed', $edit, t('Save'));
    $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'],
    )));
    $feed = db_query("SELECT * FROM {aggregator_feed} WHERE url = :url", array(
      ':url' => $edit['url'],
    ))
      ->fetchObject();
    aggregator_refresh($feed);
    $before = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField();

    // Sleep for 3 second.
    sleep(3);
    db_update('aggregator_feed')
      ->condition('fid', $feed->fid)
      ->fields(array(
      'checked' => 0,
      'hash' => '',
      'etag' => '',
      'modified' => 0,
    ))
      ->execute();
    aggregator_refresh($feed);
    $after = db_query('SELECT timestamp FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField();
    $this
      ->assertTrue($before === $after, format_string('Publish timestamp of feed item was not updated (!before === !after)', array(
      '!before' => $before,
      '!after' => $after,
    )));
  }

}
class RemoveFeedItemTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Remove feed item functionality',
      'description' => 'Remove feed items from a feed.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Tests running "remove items" from 'admin/config/services/aggregator' page.
   */
  function testRemoveFeedItem() {

    // Create a bunch of test feeds.
    $feed_urls = array();

    // No last-modified, no etag.
    $feed_urls[] = url('aggregator/test-feed', array(
      'absolute' => TRUE,
    ));

    // Last-modified, but no etag.
    $feed_urls[] = url('aggregator/test-feed/1', array(
      'absolute' => TRUE,
    ));

    // No Last-modified, but etag.
    $feed_urls[] = url('aggregator/test-feed/0/1', array(
      'absolute' => TRUE,
    ));

    // Last-modified and etag.
    $feed_urls[] = url('aggregator/test-feed/1/1', array(
      'absolute' => TRUE,
    ));
    foreach ($feed_urls as $feed_url) {
      $feed = $this
        ->createFeed($feed_url);

      // Update and remove items two times in a row to make sure that removal
      // resets all 'modified' information (modified, etag, hash) and allows for
      // immediate update.
      $this
        ->updateAndRemove($feed, 4);
      $this
        ->updateAndRemove($feed, 4);
      $this
        ->updateAndRemove($feed, 4);

      // Delete feed.
      $this
        ->deleteFeed($feed);
    }
  }

}

/**
 * Tests categorization functionality in the Aggregator module.
 */
class CategorizeFeedItemTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Categorize feed item functionality',
      'description' => 'Test feed item categorization.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Checks that children of a feed inherit a defined category.
   *
   * If a feed has a category, make sure that the children inherit that
   * categorization.
   */
  function testCategorizeFeedItem() {
    $this
      ->createSampleNodes();

    // Simulate form submission on "admin/config/services/aggregator/add/category".
    $edit = array(
      'title' => $this
        ->randomName(10),
      'description' => '',
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/category', $edit, t('Save'));
    $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'],
    )));
    $category = db_query("SELECT * FROM {aggregator_category} WHERE title = :title", array(
      ':title' => $edit['title'],
    ))
      ->fetch();
    $this
      ->assertTrue(!empty($category), 'The category found in database.');
    $link_path = 'aggregator/categories/' . $category->cid;
    $menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(
      ':link_path' => $link_path,
    ))
      ->fetch();
    $this
      ->assertTrue(!empty($menu_link), 'The menu link associated with the category found in database.');
    $feed = $this
      ->createFeed();
    db_insert('aggregator_category_feed')
      ->fields(array(
      'cid' => $category->cid,
      'fid' => $feed->fid,
    ))
      ->execute();
    $this
      ->updateFeedItems($feed, $this
      ->getDefaultFeedItemCount());
    $this
      ->getFeedCategories($feed);
    $this
      ->assertTrue(!empty($feed->categories), 'The category found in the feed.');

    // For each category of a feed, ensure feed items have that category, too.
    if (!empty($feed->categories) && !empty($feed->items)) {
      foreach ($feed->categories as $category) {
        $categorized_count = db_select('aggregator_category_item')
          ->condition('iid', $feed->items, 'IN')
          ->countQuery()
          ->execute()
          ->fetchField();
        $this
          ->assertEqual($feed->item_count, $categorized_count, 'Total items in feed equal to the total categorized feed items in database');
      }
    }

    // Delete category from feed items when category is deleted.
    $cid = reset($feed->categories);
    $categories = $this
      ->getCategories();
    $category_title = $categories[$cid]->title;

    // Delete category.
    aggregator_save_category(array(
      'cid' => $cid,
    ));

    // Assert category has been removed from feed items.
    $categorized_count = db_query("SELECT COUNT(*) FROM {aggregator_category_item} WHERE cid = :cid", array(
      ':cid' => $cid,
    ))
      ->fetchField();
    $this
      ->assertFalse($categorized_count, format_string('The category %title has been removed from feed items.', array(
      '%title' => $category_title,
    )));

    // Delete feed.
    $this
      ->deleteFeed($feed);
  }

}

/**
 * Tests importing feeds from OPML functionality for the Aggregator module.
 */
class ImportOPMLTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Import feeds from OPML functionality',
      'description' => 'Test OPML import.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Opens OPML import form.
   */
  function openImportForm() {
    db_delete('aggregator_category')
      ->execute();
    $category = $this
      ->randomName(10);
    $cid = db_insert('aggregator_category')
      ->fields(array(
      'title' => $category,
      'description' => '',
    ))
      ->execute();
    $this
      ->drupalGet('admin/config/services/aggregator/add/opml');
    $this
      ->assertText('A single OPML document may contain a collection of many feeds.', 'Found OPML help text.');
    $this
      ->assertField('files[upload]', 'Found file upload field.');
    $this
      ->assertField('remote', 'Found Remote URL field.');
    $this
      ->assertField('refresh', 'Found Refresh field.');
    $this
      ->assertFieldByName("category[{$cid}]", $cid, 'Found category field.');
  }

  /**
   * Submits form filled with invalid fields.
   */
  function validateImportFormFields() {
    $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')
      ->fetchField();
    $edit = array();
    $this
      ->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
    $this
      ->assertRaw(t('You must <em>either</em> upload a file or enter a URL.'), 'Error if no fields are filled.');
    $path = $this
      ->getEmptyOpml();
    $edit = array(
      'files[upload]' => $path,
      'remote' => file_create_url($path),
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
    $this
      ->assertRaw(t('You must <em>either</em> upload a file or enter a URL.'), 'Error if both fields are filled.');
    $edit = array(
      'remote' => 'invalidUrl://empty',
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
    $this
      ->assertText(t('This URL is not valid.'), 'Error if the URL is invalid.');
    $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')
      ->fetchField();
    $this
      ->assertEqual($before, $after, 'No feeds were added during the three last form submissions.');
  }

  /**
   * Submits form with invalid, empty, and valid OPML files.
   */
  function submitImportForm() {
    $before = db_query('SELECT COUNT(*) FROM {aggregator_feed}')
      ->fetchField();
    $form['files[upload]'] = $this
      ->getInvalidOpml();
    $this
      ->drupalPost('admin/config/services/aggregator/add/opml', $form, t('Import'));
    $this
      ->assertText(t('No new feed has been added.'), 'Attempting to upload invalid XML.');
    $edit = array(
      'remote' => file_create_url($this
        ->getEmptyOpml()),
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
    $this
      ->assertText(t('No new feed has been added.'), 'Attempting to load empty OPML from remote URL.');
    $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')
      ->fetchField();
    $this
      ->assertEqual($before, $after, 'No feeds were added during the two last form submissions.');
    db_delete('aggregator_feed')
      ->execute();
    db_delete('aggregator_category')
      ->execute();
    db_delete('aggregator_category_feed')
      ->execute();
    $category = $this
      ->randomName(10);
    db_insert('aggregator_category')
      ->fields(array(
      'cid' => 1,
      'title' => $category,
      'description' => '',
    ))
      ->execute();
    $feeds[0] = $this
      ->getFeedEditArray();
    $feeds[1] = $this
      ->getFeedEditArray();
    $feeds[2] = $this
      ->getFeedEditArray();
    $edit = array(
      'files[upload]' => $this
        ->getValidOpml($feeds),
      'refresh' => '900',
      'category[1]' => $category,
    );
    $this
      ->drupalPost('admin/config/services/aggregator/add/opml', $edit, t('Import'));
    $this
      ->assertRaw(t('A feed with the URL %url already exists.', array(
      '%url' => $feeds[0]['url'],
    )), 'Verifying that a duplicate URL was identified');
    $this
      ->assertRaw(t('A feed named %title already exists.', array(
      '%title' => $feeds[1]['title'],
    )), 'Verifying that a duplicate title was identified');
    $after = db_query('SELECT COUNT(*) FROM {aggregator_feed}')
      ->fetchField();
    $this
      ->assertEqual($after, 2, 'Verifying that two distinct feeds were added.');
    $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");
    $refresh = $category = TRUE;
    foreach ($feeds_from_db as $feed) {
      $title[$feed->url] = $feed->title;
      $url[$feed->title] = $feed->url;
      $category = $category && $feed->cid == 1;
      $refresh = $refresh && $feed->refresh == 900;
    }
    $this
      ->assertEqual($title[$feeds[0]['url']], $feeds[0]['title'], 'First feed was added correctly.');
    $this
      ->assertEqual($url[$feeds[1]['title']], $feeds[1]['url'], 'Second feed was added correctly.');
    $this
      ->assertTrue($refresh, 'Refresh times are correct.');
    $this
      ->assertTrue($category, 'Categories are correct.');
  }

  /**
   * Tests the import of an OPML file.
   */
  function testOPMLImport() {
    $this
      ->openImportForm();
    $this
      ->validateImportFormFields();
    $this
      ->submitImportForm();
  }

}

/**
 * Tests functionality of the cron process in the Aggregator module.
 */
class AggregatorCronTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Update on cron functionality',
      'description' => 'Update feeds on cron.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Adds feeds and updates them via cron process.
   */
  public function testCron() {

    // Create feed and test basic updating on cron.
    global $base_url;
    $key = variable_get('cron_key', 'drupal');
    $this
      ->createSampleNodes();
    $feed = $this
      ->createFeed();
    $this
      ->drupalGet($base_url . '/cron.php', array(
      'external' => TRUE,
      'query' => array(
        'cron_key' => $key,
      ),
    ));
    $this
      ->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField(), 'Expected number of items in database.');
    $this
      ->removeFeedItems($feed);
    $this
      ->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField(), 'Expected number of items in database.');
    $this
      ->drupalGet($base_url . '/cron.php', array(
      'external' => TRUE,
      'query' => array(
        'cron_key' => $key,
      ),
    ));
    $this
      ->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField(), 'Expected number of items in database.');

    // Test feed locking when queued for update.
    $this
      ->removeFeedItems($feed);
    db_update('aggregator_feed')
      ->condition('fid', $feed->fid)
      ->fields(array(
      'queued' => REQUEST_TIME,
    ))
      ->execute();
    $this
      ->drupalGet($base_url . '/cron.php', array(
      'external' => TRUE,
      'query' => array(
        'cron_key' => $key,
      ),
    ));
    $this
      ->assertEqual(0, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField(), 'Expected number of items in database.');
    db_update('aggregator_feed')
      ->condition('fid', $feed->fid)
      ->fields(array(
      'queued' => 0,
    ))
      ->execute();
    $this
      ->drupalGet($base_url . '/cron.php', array(
      'external' => TRUE,
      'query' => array(
        'cron_key' => $key,
      ),
    ));
    $this
      ->assertEqual(5, db_query('SELECT COUNT(*) FROM {aggregator_item} WHERE fid = :fid', array(
      ':fid' => $feed->fid,
    ))
      ->fetchField(), 'Expected number of items in database.');
  }

}

/**
 * Tests rendering functionality in the Aggregator module.
 */
class AggregatorRenderingTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Checks display of aggregator items',
      'description' => 'Checks display of aggregator items on the page.',
      'group' => 'Aggregator',
    );
  }

  /**
   * Adds a feed block to the page and checks its links.
   *
   * @todo Test the category block as well.
   */
  public function testBlockLinks() {

    // Create feed.
    $this
      ->createSampleNodes();
    $feed = $this
      ->createFeed();
    $this
      ->updateFeedItems($feed, $this
      ->getDefaultFeedItemCount());

    // Place block on page (@see block.test:moveBlockToRegion())
    // Need admin user to be able to access block admin.
    $this->admin_user = $this
      ->drupalCreateUser(array(
      'administer blocks',
      'access administration pages',
      'administer news feeds',
      'access news feeds',
    ));
    $this
      ->drupalLogin($this->admin_user);

    // Prepare to use the block admin form.
    $block = array(
      'module' => 'aggregator',
      'delta' => 'feed-' . $feed->fid,
      'title' => $feed->title,
    );
    $region = 'footer';
    $edit = array();
    $edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = $region;

    // Check the feed block is available in the block list form.
    $this
      ->drupalGet('admin/structure/block');
    $this
      ->assertFieldByName('blocks[' . $block['module'] . '_' . $block['delta'] . '][region]', '', 'Aggregator feed block is available for positioning.');

    // Position it.
    $this
      ->drupalPost('admin/structure/block', $edit, t('Save blocks'));
    $this
      ->assertText(t('The block settings have been updated.'), format_string('Block successfully moved to %region_name region.', array(
      '%region_name' => $region,
    )));

    // Confirm that the block is now being displayed on pages.
    $this
      ->drupalGet('node');
    $this
      ->assertText(t($block['title']), 'Feed block is displayed on the page.');

    // Find the expected read_more link.
    $href = 'aggregator/sources/' . $feed->fid;
    $links = $this
      ->xpath('//a[@href = :href]', array(
      ':href' => url($href),
    ));
    $this
      ->assert(isset($links[0]), format_string('Link to href %href found.', array(
      '%href' => $href,
    )));

    // Visit that page.
    $this
      ->drupalGet($href);
    $correct_titles = $this
      ->xpath('//h1[normalize-space(text())=:title]', array(
      ':title' => $feed->title,
    ));
    $this
      ->assertFalse(empty($correct_titles), 'Aggregator feed page is available and has the correct title.');

    // Set the number of news items to 0 to test that the block does not show
    // up.
    $feed->block = 0;
    aggregator_save_feed((array) $feed);

    // It is necessary to flush the cache after saving the number of items.
    drupal_flush_all_caches();

    // Check that the block is no longer displayed.
    $this
      ->drupalGet('node');
    $this
      ->assertNoText(t($block['title']), 'Feed block is not displayed on the page when number of items is set to 0.');
  }

  /**
   * Creates a feed and checks that feed's page.
   */
  public function testFeedPage() {

    // Increase the number of items published in the rss.xml feed so we have
    // enough articles to test paging.
    variable_set('feed_default_items', 30);

    // Create a feed with 30 items.
    $this
      ->createSampleNodes(30);
    $feed = $this
      ->createFeed();
    $this
      ->updateFeedItems($feed, 30);

    // Check for the presence of a pager.
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $elements = $this
      ->xpath("//ul[@class=:class]", array(
      ':class' => 'pager',
    ));
    $this
      ->assertTrue(!empty($elements), 'Individual source page contains a pager.');

    // Reset the number of items in rss.xml to the default value.
    variable_set('feed_default_items', 10);
  }

}

/**
 * Tests feed parsing in the Aggregator module.
 */
class FeedParserTestCase extends AggregatorTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Feed parser functionality',
      'description' => 'Test the built-in feed parser with valid feed samples.',
      'group' => 'Aggregator',
    );
  }
  function setUp() {
    parent::setUp();

    // Do not remove old aggregator items during these tests, since our sample
    // feeds have hardcoded dates in them (which may be expired when this test
    // is run).
    variable_set('aggregator_clear', AGGREGATOR_CLEAR_NEVER);
  }

  /**
   * Tests a feed that uses the RSS 0.91 format.
   */
  function testRSS091Sample() {
    $feed = $this
      ->createFeed($this
      ->getRSS091Sample());
    aggregator_refresh($feed);
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $this
      ->assertResponse(200, format_string('Feed %name exists.', array(
      '%name' => $feed->title,
    )));
    $this
      ->assertText('First example feed item title');
    $this
      ->assertLinkByHref('http://example.com/example-turns-one');
    $this
      ->assertText('First example feed item description.');

    // Several additional items that include elements over 255 characters.
    $this
      ->assertRaw("Second example feed item title.");
    $this
      ->assertText('Long link feed item title');
    $this
      ->assertText('Long link feed item description');
    $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');
    $this
      ->assertText('Long author feed item title');
    $this
      ->assertText('Long author feed item description');
    $this
      ->assertLinkByHref('http://example.com/long/author');
  }

  /**
   * Tests a feed that uses the Atom format.
   */
  function testAtomSample() {
    $feed = $this
      ->createFeed($this
      ->getAtomSample());
    aggregator_refresh($feed);
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $this
      ->assertResponse(200, format_string('Feed %name exists.', array(
      '%name' => $feed->title,
    )));
    $this
      ->assertText('Atom-Powered Robots Run Amok');
    $this
      ->assertLinkByHref('http://example.org/2003/12/13/atom03');
    $this
      ->assertText('Some text.');
    $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.');
  }

  /**
   * Tests a feed that uses HTML entities in item titles.
   */
  function testHtmlEntitiesSample() {
    $feed = $this
      ->createFeed($this
      ->getHtmlEntitiesSample());
    aggregator_refresh($feed);
    $this
      ->drupalGet('aggregator/sources/' . $feed->fid);
    $this
      ->assertResponse(200, format_string('Feed %name exists.', array(
      '%name' => $feed->title,
    )));
    $this
      ->assertRaw("Quote&quot; Amp&amp;");
  }

}

Classes

Namesort descending Description
AddFeedTestCase Tests adding aggregator feeds.
AggregatorConfigurationTestCase Tests functionality of the configuration settings in the Aggregator module.
AggregatorCronTestCase Tests functionality of the cron process in the Aggregator module.
AggregatorRenderingTestCase Tests rendering functionality in the Aggregator module.
AggregatorTestCase Defines a base class for testing the Aggregator module.
CategorizeFeedItemTestCase Tests categorization functionality in the Aggregator module.
CategorizeFeedTestCase Tests the categorize feed functionality in the Aggregator module.
FeedParserTestCase Tests feed parsing in the Aggregator module.
ImportOPMLTestCase Tests importing feeds from OPML functionality for the Aggregator module.
RemoveFeedItemTestCase
RemoveFeedTestCase Tests functionality for removing feeds in the Aggregator module.
UpdateFeedItemTestCase Tests functionality of updating a feed item in the Aggregator module.
UpdateFeedTestCase Tests functionality of updating the feed in the Aggregator module.