<?php

/**
 * @file
 * Scheduler module test case file.
 */

/**
 * Provides common helper methods for Scheduler module tests.
 */
abstract class SchedulerTestBase extends DrupalWebTestCase {
  /**
   * The profile to install as a basis for testing.
   *
   * @var string
   */
  protected $profile = 'testing';

  /**
   * A user with administration rights.
   *
   * @var object
   */
  protected $admin_user;

  /**
   * Common settings and options.
   */
  function commonSettings() {

    // Create a 'Basic Page' content type.
    $this->drupalCreateContentType(array('type' => 'page', 'name' => t('Basic page')));

    // Create an administrator user.
    $this->admin_user = $this->drupalCreateUser(array(
      'access content',
      'administer scheduler',
      'create page content',
      'edit own page content',
      'delete own page content',
      'view own unpublished content',
      'administer nodes',
      'schedule publishing of nodes'
    ));

    // Add scheduler functionality to the page node type.
    variable_set('scheduler_publish_enable_page', 1);
    variable_set('scheduler_unpublish_enable_page', 1);
    variable_set('scheduler_field_type', 'textfield');
  }

  /**
   * Helper function for testScheduler(). Schedules content and asserts status.
   *
   * @param array $edit
   *   Node data, as if it was sent from the edit form.
   * @param bool $scheduler_cron_only
   *   TRUE to only run Scheduler cron, FALSE to run default full Drupal cron.
   */
  function helpTestScheduler($edit, $scheduler_cron_only = FALSE) {
    // Add a page.
    $langcode = LANGUAGE_NONE;
    $title = $this->randomName();
    $edit["title"] = $title;
    $body = $this->randomName();
    $edit["body[$langcode][0][value]"] = $body;
    $this->drupalLogin($this->admin_user);
    $this->drupalPost('node/add/page', $edit, t('Save'));
    $node = $this->drupalGetNodeByTitle($title);
    // Show the specific page for an anonymous visitor, then assert that the
    // node is correctly published or unpublished.
    $this->drupalLogout();
    $this->drupalGet("node/{$node->nid}");
    if (isset($edit['publish_on'])) {
      $key = 'publish_on';
      $this->assertResponse(403, t('Node is unpublished'));
    }
    else {
      $key = 'unpublish_on';
      $this->assertText($body, t('Node is published'));
    }
    // Verify that the scheduler table is not empty.
    $this->assertTrue(db_query_range('SELECT 1 FROM {scheduler}', 0, 1)->fetchField(), 'Scheduler table is not empty');
    // Modify the scheduler row to a time far enough in the past because
    // scheduler_cron uses REQUEST_TIME and our timestamp has to be before that.
    db_update('scheduler')->fields(array($key => time() - 3600))->execute();
    if ($scheduler_cron_only) {
      scheduler_cron();
    }
    else {
      $this->cronRun();
    }
    // Verify that the scheduler table is empty.
    $this->assertFalse(db_query_range('SELECT 1 FROM {scheduler}', 0, 1)->fetchField(), 'Scheduler table is empty');
    // Show the specific page for an anonymous visitor, then assert that the
    // node is correctly published or unpublished.
    $this->drupalGet("node/{$node->nid}");
    if (isset($edit['publish_on'])) {
      $this->assertText($body, t('Node is published'));
    }
    else {
      $this->assertResponse(403, t('Node is unpublished'));
    }
  }

  /**
   * Simulates the scheduled (un)publication of a node.
   *
   * @param object $node
   *   The node to schedule.
   * @param string $action
   *   The action to perform: either 'publish' or 'unpublish'. Defaults to
   *   'publish'.
   *
   * @return object
   *   The updated node, after scheduled (un)publication.
   */
  function schedule($node, $action = 'publish') {
    // Simulate scheduling by setting the (un)publication date in the past and
    // running cron.
    $node->{$action . '_on'} = strtotime('-1 day');
    node_save($node);
    scheduler_cron();
    return node_load($node->nid, NULL, TRUE);
  }

  /**
   * Check if the latest revision log message of a node matches a given string.
   *
   * @param int $nid
   *   The node id of the node to check.
   * @param string $value
   *   The value with which the log message will be compared.
   * @param string $message
   *   The message to display along with the assertion.
   * @param string $group
   *   The type of assertion - examples are "Browser", "PHP".
   *
   * @return
   *   TRUE if the assertion succeeded, FALSE otherwise.
   */
  function assertRevisionLogMessage($nid, $value, $message = '', $group = 'Other') {
    $log_message = db_select('node_revision', 'r')
      ->fields('r', array('log'))
      ->condition('nid', $nid)
      ->orderBy('vid', 'DESC')
      ->range(0, 1)
      ->execute()
      ->fetchColumn();
    return $this->assertEqual($log_message, $value, $message, $group);
  }

  /**
   * Check if the number of revisions for a node matches a given value.
   *
   * @param int $nid
   *   The node id of the node to check.
   * @param string $value
   *   The value with which the number of revisions will be compared.
   * @param string $message
   *   The message to display along with the assertion.
   * @param string $group
   *   The type of assertion - examples are "Browser", "PHP".
   *
   * @return
   *   TRUE if the assertion succeeded, FALSE otherwise.
   */
  function assertRevisionCount($nid, $value, $message = '', $group = 'Other') {
    $count = db_select('node_revision', 'r')
      ->fields('r', array('vid'))
      ->condition('nid', $nid)
      ->countQuery()
      ->execute()
      ->fetchColumn();
    return $this->assertEqual($count, $value, $message, $group);
  }
}

/**
 * Tests the scheduler interface.
 */
class SchedulerFunctionalTest extends SchedulerTestBase {
  /**
   * {@inheritdoc}
   */
  public static function getInfo() {
    return array(
      'name' => 'Scheduler functionality',
      'description' => 'Publish/unpublish on time.',
      'group' => 'Scheduler',
    );
  }

  /**
   * {@inheritdoc}
   */
  function setUp() {
    parent::setUp('scheduler');
    parent::commonSettings();
  }

  /**
   * Tests basic scheduling of content.
   *
   * @param bool $scheduler_cron_only
   *   TRUE to only run Scheduler cron, FALSE to run default full Drupal cron.
   */
  function testScheduler($scheduler_cron_only = FALSE) {
    // Create node values. Set time to one hour in the future.
    $edit = array(
      'publish_on' => format_date(time() + 3600, 'custom', 'Y-m-d H:i:s'),
      'status' => 1,
      'promote' => 1,
    );
    // Test scheduled publishing.
    $this->helpTestScheduler($edit, $scheduler_cron_only);
    // Test scheduled unpublishing.
    $edit['unpublish_on'] = $edit['publish_on'];
    unset($edit['publish_on']);
    $this->helpTestScheduler($edit, $scheduler_cron_only);
  }

  /**
   * Tests scheduler when not all cron tasks are run during cron.
   *
   * Verify that we can set variable 'scheduler_cache_clear_all' so the page
   * cache is still cleared.
   *
   * @uses testScheduler()
   */
  function testSchedulerWithOnlySchedulerCronAndAnonymousPageCache() {
    // Cache pages for anonymous users.
    variable_set('cache', 1);
    // Instruct scheduler to clear caches itself, instead of relying on
    // system_cron.
    variable_set('scheduler_cache_clear_all', 1);
    // Instruct the helper method to run only the scheduler cron.
    $scheduler_cron_only = TRUE;

    $this->testScheduler($scheduler_cron_only);
  }

  /**
   * Test the different options for past publication dates.
   */
  public function testSchedulerPastDates() {
    // Log in.
    $this->drupalLogin($this->admin_user);

    // Create an unpublished page node.
    $node = $this->drupalCreateNode(array('type' => 'page', 'status' => FALSE));

    // Test the default behavior: an error message should be shown when the user
    // enters a publication date that is in the past.
    $edit = array(
      'title' => $this->randomName(),
      'publish_on' => format_date(strtotime('-1 day'), 'custom', 'Y-m-d H:i:s'),
    );
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
    $this->assertRaw(t("The 'publish on' date must be in the future"), 'An error message is shown when the publication date is in the past and the "error" behavior is chosen.');

    // Test the 'publish' behavior: the node should be published immediately.
    variable_set('scheduler_publish_past_date_page', 'publish');
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
    $this->assertNoRaw(t("The 'publish on' date must be in the future"), 'No error message is shown when the publication date is in the past and the "publish" behavior is chosen.');
    $this->assertRaw(t('@type %title has been updated.', array('@type' => t('Basic page'), '%title' => check_plain($edit['title']))), 'The node is saved successfully when the publication date is in the past and the "publish" behavior is chosen.');

    // Reload the changed node and check that it is published.
    $node = node_load($node->nid, NULL, TRUE);
    $this->assertTrue($node->status, 'The node has been published immediately when the publication date is in the past and the "publish" behavior is chosen.');

    // Test the 'schedule' behavior: the node should be unpublished and become
    // published on the next cron run.
    variable_set('scheduler_publish_past_date_page', 'schedule');
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
    $this->assertNoRaw(t("The 'publish on' date must be in the future"), 'No error message is shown when the publication date is in the past and the "schedule" behavior is chosen.');
    $this->assertRaw(t('@type %title has been updated.', array('@type' => t('Basic page'), '%title' => check_plain($edit['title']))), 'The node is saved successfully when the publication date is in the past and the "schedule" behavior is chosen.');
    $this->assertRaw(t('This post is unpublished and will be published @publish_time.', array('@publish_time' => $edit['publish_on'])), 'The node is scheduled to be published when the publication date is in the past and the "schedule" behavior is chosen.');

    // Reload the node and check that it is unpublished but scheduled correctly.
    $node = node_load($node->nid, NULL, TRUE);
    $this->assertFalse($node->status, 'The node has been unpublished when the publication date is in the past and the "schedule" behavior is chosen.');
    $this->assertEqual(format_date($node->publish_on, 'custom', 'Y-m-d H:i:s'), $edit['publish_on'], 'The node is scheduled for the required date');

    // Simulate a cron run and check that the node is published.
    scheduler_cron();
    $node = node_load($node->nid, NULL, TRUE);
    $this->assertTrue($node->status, 'The node with publication date in the past and the "schedule" behavior has now been published by cron.');
  }

  /**
   * Tests the creation of new revisions on scheduling.
   */
  public function testRevisioning() {
    // Create a scheduled node that is not automatically revisioned.
    $created = strtotime('-2 day');
    $settings = array(
      'revision' => 0,
      'created' => $created,
    );
    $node = $this->drupalCreateNode($settings);

    // First test scheduled publication with revisioning disabled.
    $node = $this->schedule($node);
    $this->assertRevisionCount($node->nid, 1, 'No new revision was created when a node was published with revisioning disabled.');

    // Test scheduled unpublication.
    $node = $this->schedule($node, 'unpublish');
    $this->assertRevisionCount($node->nid, 1, 'No new revision was created when a node was unpublished with revisioning disabled.');

    // Enable revisioning.
    variable_set('scheduler_publish_revision_page', 1);
    variable_set('scheduler_unpublish_revision_page', 1);

    // Test scheduled publication with revisioning enabled.
    $node = $this->schedule($node);
    $this->assertRevisionCount($node->nid, 2, 'A new revision was created when revisioning is enabled.');
    $expected_message = t('Node published by Scheduler on @now. Previous creation date was @date.', array(
      '@now' => format_date(REQUEST_TIME, 'short'),
      '@date' => format_date($created, 'short'),
    ));
    $this->assertRevisionLogMessage($node->nid, $expected_message, 'The correct message was found in the node revision log after scheduled publishing.');

    // Test scheduled unpublication with revisioning enabled.
    $node = $this->schedule($node, 'unpublish');
    $this->assertRevisionCount($node->nid, 3, 'A new revision was created when a node was unpublished with revisioning enabled.');
    $expected_message = t('Node unpublished by Scheduler on @now. Previous change date was @date.', array(
      '@now' => format_date(REQUEST_TIME, 'short'),
      '@date' => format_date(REQUEST_TIME, 'short'),
    ));
    $this->assertRevisionLogMessage($node->nid, $expected_message, 'The correct message was found in the node revision log after scheduled unpublishing.');
  }

  /**
   * Tests if options can both be displayed as extra fields and vertical tabs.
   */
  function testExtraFields() {
    $this->drupalLogin($this->admin_user);

    // Test if the options are shown as vertical tabs by default.
    $this->drupalGet('node/add/page');
    $this->assertTrue($this->xpath('//div[contains(@class, "vertical-tabs-panes")]/fieldset[@id = "edit-scheduler-settings"]'), 'By default the scheduler options are shown as a vertical tab.');

    // Test if the options are shown as extra fields when configured to do so.
    variable_set('scheduler_use_vertical_tabs_page', 0);
    $this->drupalGet('node/add/page');
    $this->assertFalse($this->xpath('//div[contains(@class, "vertical-tabs-panes")]/fieldset[@id = "edit-scheduler-settings"]'), 'The scheduler options are not shown as a vertical tab when they are configured to show as an extra field.');
    $this->assertTrue($this->xpath('//fieldset[@id = "edit-scheduler-settings" and contains(@class, "collapsed")]'), 'The scheduler options are shown as a collapsed fieldset when they are configured to show as an extra field.');

    // Test the option to expand the fieldset.
    variable_set('scheduler_expand_fieldset_page', 1);
    $this->drupalGet('node/add/page');
    $this->assertFalse($this->xpath('//div[contains(@class, "vertical-tabs-panes")]/fieldset[@id = "edit-scheduler-settings"]'), 'The scheduler options are not shown as a vertical tab when they are configured to show as an expanded fieldset.');
    $this->assertTrue($this->xpath('//fieldset[@id = "edit-scheduler-settings" and not(contains(@class, "collapsed"))]'), 'The scheduler options are shown as an expanded fieldset.');
  }



  /**
   * Tests creating and editing nodes with required scheduling enabled.
   */
  function testRequiredScheduling() {
    $this->drupalLogin($this->admin_user);

    // Define test scenarios with expected results.
    $test_cases = array(
      // The 1-10 numbering used below matches the test cases described in
      // http://drupal.org/node/1198788#comment-7816119

      // A. Test scenarios that require scheduled publishing.

      // When creating a new unpublished node it is required to enter a
      // publication date.
      array(
        'id' => 1,
        'required' => 'publish',
        'operation' => 'add',
        'status' => 0,
        'expected' => 'required',
        'message' => 'When scheduled publishing is required and a new unpublished node is created, entering a date in the publish on field is required.',
      ),

      // When creating a new published node it is required to enter a
      // publication date. The node will be unpublished on form submit.
      array(
        'id' => 2,
        'required' => 'publish',
        'operation' => 'add',
        'status' => 1,
        'expected' => 'required',
        'message' => 'When scheduled publishing is required and a new published node is created, entering a date in the publish on field is required.',
      ),

      // When editing a published node it is not needed to enter a publication
      // date since the node is already published.
      array(
        'id' => 3,
        'required' => 'publish',
        'operation' => 'edit',
        'scheduled' => 0,
        'status' => 1,
        'expected' => 'not required',
        'message' => 'When scheduled publishing is required and an existing published, unscheduled node is edited, entering a date in the publish on field is not required.',
      ),

      // When editing an unpublished node that is scheduled for publication it
      // is required to enter a publication date.
      array(
        'id' => 4,
        'required' => 'publish',
        'operation' => 'edit',
        'scheduled' => 1,
        'status' => 0,
        'expected' => 'required',
        'message' => 'When scheduled publishing is required and an existing unpublished, scheduled node is edited, entering a date in the publish on field is required.',
      ),

      // When editing an unpublished node that is not scheduled for publication
      // it is not required to enter a publication date since this means that
      // the node has already gone through a publication > unpublication cycle.
      array(
        'id' => 5,
        'required' => 'publish',
        'operation' => 'edit',
        'scheduled' => 0,
        'status' => 0,
        'expected' => 'not required',
        'message' => 'When scheduled publishing is required and an existing unpublished, unscheduled node is edited, entering a date in the publish on field is not required.',
      ),

      // B. Test scenarios that require scheduled unpublishing.

      // When creating a new unpublished node it is required to enter an
      // unpublication date since it is to be expected that the node will be
      // published at some point and should subsequently be unpublished.
      array(
        'id' => 6,
        'required' => 'unpublish',
        'operation' => 'add',
        'status' => 0,
        'expected' => 'required',
        'message' => 'When scheduled unpublishing is required and a new unpublished node is created, entering a date in the unpublish on field is required.',
      ),

      // When creating a new published node it is required to enter an
      // unpublication date.
      array(
        'id' => 7,
        'required' => 'unpublish',
        'operation' => 'add',
        'status' => 1,
        'expected' => 'required',
        'message' => 'When scheduled unpublishing is required and a new published node is created, entering a date in the unpublish on field is required.',
      ),

      // When editing a published node it is required to enter an unpublication
      // date.
      array(
        'id' => 8,
        'required' => 'unpublish',
        'operation' => 'edit',
        'scheduled' => 0,
        'status' => 1,
        'expected' => 'required',
        'message' => 'When scheduled unpublishing is required and an existing published, unscheduled node is edited, entering a date in the unpublish on field is required.',
      ),

      // When editing an unpublished node that is scheduled for publication it
      // it is required to enter an unpublication date.
      array(
        'id' => 9,
        'required' => 'unpublish',
        'operation' => 'edit',
        'scheduled' => 1,
        'status' => 0,
        'expected' => 'required',
        'message' => 'When scheduled unpublishing is required and an existing unpublished, scheduled node is edited, entering a date in the unpublish on field is required.',
      ),

      // When editing an unpublished node that is not scheduled for publication
      // it is not required to enter an unpublication date since this means that
      // the node has already gone through a publication - unpublication cycle.
      array(
        'id' => 10,
        'required' => 'unpublish',
        'operation' => 'edit',
        'scheduled' => 0,
        'status' => 0,
        'expected' => 'not required',
        'message' => 'When scheduled unpublishing is required and an existing unpublished, unscheduled node is edited, entering a date in the unpublish on field is not required.',
      ),
    );

    foreach ($test_cases as $test_case) {
      // Enable required (un)publishing as stipulated by the test case.
      variable_set('scheduler_publish_required_page', $test_case['required'] == 'publish');
      variable_set('scheduler_unpublish_required_page', $test_case['required'] == 'unpublish');

      // Set the default node status, used when creating a new node.
      $node_options_page = !empty($test_case['status']) ? array('status') : array();
      variable_set('node_options_page', $node_options_page);

      // To assist viewing and analysing the generated test result pages create
      // a text string showing all the test case parameters.
      $title_data = array();
      foreach ($test_case as $key => $value) {
        if ($key != 'message') {
          $title_data[] = $key . ' = ' . $value;
        }
      }
      $title = implode(', ', $title_data);

      // If the test case requires editing a node, we need to create one first.
      if ($test_case['operation'] == 'edit') {
        $options = array(
          'title' => $title,
          'type' => 'page',
          'status' => $test_case['status'],
          'publish_on' => !empty($test_case['scheduled']) ? strtotime('+ 1 day') : 0,
        );
        $node = $this->drupalCreateNode($options);
      }

      // Make sure the publication date fields are empty so we can check if they
      // throw form validation errors when they are required.
      $edit = array(
        'title' => $title,
        'publish_on' => '',
        'unpublish_on' => '',
      );
      $path = $test_case['operation'] == 'add' ? 'node/add/page' : 'node/' . $node->nid . '/edit';
      $this->drupalPost($path, $edit, t('Save'));

      // Check for the expected result.
      switch ($test_case['expected']) {
        case 'required':
          $string = t('!name field is required.', array('!name' => ucfirst($test_case['required']) . ' on'));
          $this->assertRaw($string, $test_case['id'] . '. ' . $test_case['message']);
          break;

        case 'not required':
          $string = '@type %title has been ' . ($test_case['operation'] == 'add' ? 'created' : 'updated') . '.';
          $args = array('@type' => 'Basic page', '%title' => $title);
          $this->assertRaw(t($string, $args), $test_case['id'] . '. ' . $test_case['message']);
          break;
      }
    }
  }

  /**
   * Tests the validation when editing a node.
   *
   * The 'required' checks and 'dates in the past' checks are handled in other
   * tests. This test checks validation when the two fields interact.
   */
  function testValidationDuringEdit() {
    $this->drupalLogin($this->admin_user);

    // Set unpublishing to be required.
    variable_set('scheduler_unpublish_required_page', TRUE);

    // Create an unpublished page node, then edit the node and check that if a
    // publish-on date is entered then an unpublish-on date is also needed.
    $node = $this->drupalCreateNode(array('type' => 'page', 'status' => FALSE));
    $edit = array(
      'publish_on' => date('Y-m-d H:i:s', strtotime('+1 day', REQUEST_TIME)),
    );
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
    $this->assertRaw(t("If you set a 'publish-on' date then you must also set an 'unpublish-on' date."), 'Validation prevents entering a publish-on date with no unpublish-on date if unpublishing is required.');

    // Create an unpublished page node, then edit the node and check that if the
    // status is changed to published, then an unpublish-on date is also needed.
    $node = $this->drupalCreateNode(array('type' => 'page', 'status' => FALSE));
    $edit = array(
      'status' => TRUE,
    );
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
    $this->assertRaw(t("To publish this node you must also set an 'unpublish-on' date."), 'Validation prevents publishing the node directly without an unpublish-on date if unpublishing is required.');

    // Create an unpublished page node, edit the node and check that if both
    // dates are entered then the unpublish date is later than the publish date.
    $node = $this->drupalCreateNode(array('type' => 'page', 'status' => FALSE));
    $edit = array(
      'publish_on' => date('Y-m-d H:i:s', strtotime('+2 day', REQUEST_TIME)),
      'unpublish_on' => date('Y-m-d H:i:s', strtotime('+1 day', REQUEST_TIME)),
    );
    $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
    $this->assertRaw(t("The 'unpublish on' date must be later than the 'publish on' date."), 'Validation prevents entering an unpublish-on date which is earlier than the publish-on date.');

  }



  /**
   * Tests the deletion of a scheduled node.
   *
   */
  public function testScheduledNodeDelete() {
    // Log in.
    $this->drupalLogin($this->admin_user);

    // 1. Test if it is possible to delete a node that does not have a
    // publication date set, when scheduled publishing is required, and likewise
    // for unpublishing.
    // @see https://drupal.org/node/1614880

    // Create a published and an unpublished node, both without scheduling.
    $published_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 1));
    $unpublished_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 0));

    // Make scheduled publishing and unpublishing required.
    variable_set('scheduler_publish_required_page', TRUE);
    variable_set('scheduler_unpublish_required_page', TRUE);

    // Check that deleting the nodes does not throw form validation errors.
    // The text 'error message' is used in a header h2 html tag which is
    // normally made hidden from browsers but will be in the page source.
    // It is also good when testing for the absense of something to also test
    // for the presence of text, hence the second assertion for each check.
    $this->drupalPost('node/' . $published_node->nid . '/edit', array(), t('Delete'));
    $this->assertNoRaw(t('Error message'), 'No error messages are shown when trying to delete a published node with no scheduling information.');
    $this->assertRaw(t('Are you sure you want to delete'), 'The deletion warning message is shown immediately when trying to delete a published node with no scheduling information.');

    $this->drupalPost('node/' . $unpublished_node->nid . '/edit', array(), t('Delete'));
    $this->assertNoRaw(t('Error message'), 'No error messages are shown when trying to delete an unpublished node with no scheduling information.');
    $this->assertRaw(t('Are you sure you want to delete'), 'The deletion warning message is shown immediately when trying to delete an unpublished node with no scheduling information.');

    // 2. Test that nodes can be deleted with no validation errors if the
    // dates are in the past.
    // @see http://drupal.org/node/2627370

    // Turn off required publishing and unpublishing.
    variable_set('scheduler_publish_required_page', FALSE);
    variable_set('scheduler_unpublish_required_page', FALSE);

    // Create nodes with publish_on and unpublish_on dates in the past.
    $published_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 1, 'unpublish_on' => strtotime('- 2 day')));
    $unpublished_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 0, 'publish_on' => strtotime('- 2 day')));

    // Attempt to delete the published node and check for no validation error.
    $this->drupalPost('node/' . $published_node->nid . '/edit', array(), t('Delete'));
    $this->assertNoRaw(t('Error message'), 'No error messages are shown when trying to delete a node with an unpublish date in the past.');
    $this->assertRaw(t('Are you sure you want to delete'), 'The deletion warning message is shown immediately when trying to delete a node with an unpublish date in the past.');

    // Attempt to delete the unpublished node and check for no validation error.
    $this->drupalPost('node/' . $unpublished_node->nid . '/edit', array(), t('Delete'));
    $this->assertNoRaw(t('Error message'), 'No error messages are shown when trying to delete a node with a publish date in the past.');
    $this->assertRaw(t('Are you sure you want to delete'), 'The deletion warning message is shown immediately when trying to delete a node with a publish date in the past.');

  }

  /**
   * Tests meta-information on scheduled nodes.
   *
   * When nodes are scheduled for unpublication, an X-Robots-Tag HTTP header is
   * sent, alerting crawlers about when an item expires and should be removed
   * from search results.
   */
  public function testMetaInformation() {
    // Log in.
    $this->drupalLogin($this->admin_user);

    // Create a published node without scheduling.
    $published_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 1));
    $this->drupalGet('node/' . $published_node->nid);

    // Since we did not set an unpublish date, there should be no X-Robots-Tag
    // header on the response.
    $this->assertFalse($this->drupalGetHeader('X-Robots-Tag'), 'X-Robots-Tag is not present when no unpublish date is set.');

    // Set a scheduler unpublish date on the node.
    $unpublish_date = strtotime('+1 day');
    $edit = array(
      'unpublish_on' => format_date($unpublish_date, 'custom', 'Y-m-d H:i:s'),
    );
    $this->drupalPost('node/' . $published_node->nid . '/edit', $edit, t('Save'));

    // The node page should now have an X-Robots-Tag header with an
    // unavailable_after-directive and RFC850 date- and time-value.
    $this->drupalGet('node/' . $published_node->nid);
    $robots_tag = $this->drupalGetHeader('X-Robots-Tag');
    $this->assertEqual($robots_tag, 'unavailable_after: ' . date(DATE_RFC850, $unpublish_date), 'X-Robots-Tag is present with correct timestamp derived from unpublish_on date.');
  }

  /**
   * Tests that users without permission do not see the scheduler date fields.
   */
  public function testPermissions() {
    // Create a user who can add the content type but who does not have the
    // permission to use the scheduler functionality.
    $this->webUser = $this->drupalCreateUser(array(
      'access content',
      'create page content',
      'edit own page content',
      'view own unpublished content',
      'administer nodes',
    ));
    $this->drupalLogin($this->webUser);

    // Set the defaults for a new node. Nothing in array means all OFF.
    // 'status', 'promote', 'sticky'
    variable_set('node_options_page', array());

    // Check that neither of the fields are displayed when creating a node.
    $this->drupalGet('node/add/page');
    $this->assertNoFieldByName('publish_on', '', 'The Publish-on field is not shown for users who do not have permission to schedule content');
    $this->assertNoFieldByName('unpublish_on', '', 'The Unpublish-on field is not shown for users who do not have permission to schedule content');

    // Initially run tests when publishing and unpublishing are not required.
    variable_set('scheduler_publish_required_page', FALSE);
    variable_set('scheduler_unpublish_required_page', FALSE);

    // Check that a new node can be saved and published.
    $title = $this->randomString(15);
    $this->drupalPost('node/add/page', array('title' => $title, 'status' => TRUE), t('Save'));
    $this->assertRaw(t('@type %title has been created.', array('@type' => 'Basic page', '%title' => $title)), 'A node can be created and published when the user does not have scheduler permissions, and scheduling is not required.');
    $node = $this->drupalGetNodeByTitle($title);
    $this->assertTrue($node->status, 'The new node is published.');

    // Check that a new node can be saved as unpublished.
    $title = $this->randomString(15);
    $this->drupalPost('node/add/page', array('title' => $title, 'status' => FALSE), t('Save'));
    $this->assertRaw(t('@type %title has been created.', array('@type' => 'Basic page', '%title' => $title)), 'A node can be created and saved as unpublished when the user does not have scheduler permissions, and scheduling is not required.');
    $node = $this->drupalGetNodeByTitle($title);
    $this->assertFalse($node->status, 'The new node is unpublished.');

    // Set publishing and unpublishing to required, to make it a stronger test.
    variable_set('scheduler_publish_required_page', TRUE);
    variable_set('scheduler_unpublish_required_page', TRUE);

    // @TODO Add tests when scheduled publishing and unpublishing are required.
    // Cannot be done until we make a decision on what 'required'  means.
    // @see https://www.drupal.org/node/2707411
    // "Conflict between 'required publishing' and not having scheduler permission"
  }

  /**
   * Tests Scheduler token support.
   */
  public function testTokenReplacement() {
    // Log in.
    $this->drupalLogin($this->admin_user);

    // Define timestamps for consistent use when repeated throughout this test.
    $publish_on_timestamp = REQUEST_TIME + 3600;
    $unpublish_on_timestamp = REQUEST_TIME + 7200;

    // Create an unpublished page with scheduled dates.
    $settings = array(
      'type' => 'page',
      'status' => FALSE,
      'publish_on' => $publish_on_timestamp,
      'unpublish_on' => $unpublish_on_timestamp,
    );
    $node = $this->drupalCreateNode($settings);
    // Show that the node is scheduled.
    $this->drupalGet('admin/config/content/scheduler/list');

    // Create array of test case data.
    $test_cases = array(
      array('token_format' => '', 'date_format' => 'medium', 'custom' => ''),
      array('token_format' => ':long', 'date_format' => 'long', 'custom' => ''),
      array('token_format' => ':raw', 'date_format' => 'custom', 'custom' => 'U'),
      array('token_format' => ':custom:jS F g:ia e O', 'date_format' => 'custom', 'custom' => 'jS F g:ia e O'),
    );

    $langcode = LANGUAGE_NONE;
    foreach ($test_cases as $test_data) {
      // With each of the test cases, test using both numeric and string input.
      foreach (array('numeric', 'string') as $test_data['input_type']) {
        if ($test_data['input_type'] == 'numeric') {
          // Edit the node and set the body tokens to use the format being
          // tested. The tokens are not replaced automatically when the node is
          // viewed, but using the body is a convenient way to store the data.
          $edit = array(
            "body[$langcode][0][value]" => 'Publish on: [node:scheduler-publish' . $test_data['token_format'] . ']. Unpublish on: [node:scheduler-unpublish' . $test_data['token_format'] . '].',
          );
          $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
          $this->drupalGet('node/' . $node->nid);
          // Refresh the node. Third parameter TRUE to reset cache.
          $node = node_load($node->nid, NULL, TRUE);
        }
        else {
          // Replicate the scheduler fields as if just input by a user during
          // edit, before hook_node_presave() has been executed.
          // @see https://www.drupal.org/node/2750467
          $node->publish_on = format_date($publish_on_timestamp, 'custom', 'Y-m-d H:i:s');
          $node->unpublish_on = format_date($unpublish_on_timestamp, 'custom', 'Y-m-d H:i:s');
        }

        // Get the node body value after tokens have been replaced.
        $token_output = token_replace($node->body[$langcode][0]['value'], array('node' => $node));

        // Create the expected text for the body.
        $publish_on_date = format_date($publish_on_timestamp, $test_data['date_format'], $test_data['custom']);
        $unpublish_on_date = format_date($unpublish_on_timestamp, $test_data['date_format'], $test_data['custom']);
        $expected_output = 'Publish on: ' . $publish_on_date . '. Unpublish on: ' . $unpublish_on_date . '.';

        // Check that the actual text matches the expected value.
        $tested_format = $test_data['token_format'] ? '"' . $test_data['token_format'] . '"' : 'default';
        $this->assertEqual($token_output, $expected_output, 'Scheduler tokens replaced correctly for ' . $tested_format . ' format with ' . $test_data['input_type'] . ' input data.');
      }
    }
  }
}

/**
 * Tests the components of the scheduler interface which use the date module
 */
class SchedulerDateCombinedFunctionalTest extends SchedulerTestBase {
  /**
   * {@inheritdoc}
   */
  public static function getInfo() {
    return array(
      'name' => 'Scheduler date functionalities',
      'description' => 'Scheduler functionalities which require the date module.',
      'group' => 'Scheduler',
    );
  }

  /**
   * {@inheritdoc}
   */
  function setUp() {
    parent::setUp('date', 'date_popup', 'scheduler');
    parent::commonSettings();
  }

  /**
   * Asserts that the default time works as expected.
   */
  protected function assertDefaultTime() {
    // Define the form fields and date formats we will test according to whether
    // date popups have been enabled or not.
    $using_popup = variable_get('scheduler_field_type', 'date_popup') == 'date_popup';
    $publish_date_field = $using_popup ? 'publish_on[date]' : 'publish_on';
    $unpublish_date_field = $using_popup ? 'unpublish_on[date]' : 'unpublish_on';
    $publish_time_field = $using_popup ? 'publish_on[time]' : 'publish_on';
    $unpublish_time_field = $using_popup ? 'unpublish_on[time]' : 'unpublish_on';
    $time_format = $using_popup ? 'H:i:s' : 'Y-m-d H:i:s';

    // We cannot easily test the exact validation messages as they contain the
    // REQUEST_TIME of the POST request, which can be one or more seconds in the
    // past. Best we can do is check the fixed part of the message as it is when
    // passed to t(). This will only work in English.
    $publish_validation_message = $using_popup ? t('The value input for field %field is invalid:', array('%field' => 'Publish on')) : "The 'publish on' value does not match the expected format of";
    $unpublish_validation_message = $using_popup ? t('The value input for field %field is invalid:', array('%field' => 'Unpublish on')) : "The 'unpublish on' value does not match the expected format of";

    // First test with the "date only" functionality disabled.
    $this->drupalPost('admin/config/content/scheduler', array('scheduler_allow_date_only' => FALSE), t('Save configuration'));

    // Test if entering a time is required.
    $edit = array(
      'title' => $this->randomName(),
      $publish_date_field => date('Y-m-d', strtotime('+1 day', REQUEST_TIME)),
      $unpublish_date_field => date('Y-m-d', strtotime('+2 day', REQUEST_TIME)),
    );
    $this->drupalPost('node/add/page', $edit, t('Save'));

    $this->assertRaw($publish_validation_message, 'By default it is required to enter a time when scheduling content for publication.');
    $this->assertRaw($unpublish_validation_message, 'By default it is required to enter a time when scheduling content for unpublication.');

    // Allow the user to enter only the date and repeat the test.
    $this->drupalPost('admin/config/content/scheduler', array('scheduler_allow_date_only' => TRUE), t('Save configuration'));

    $this->drupalPost('node/add/page', $edit, t('Save'));
    $this->assertNoRaw("The 'publish on' value does not match the expected format of", 'If the default time option is enabled the user can skip the time when scheduling content for publication.');
    $this->assertNoRaw("The 'unpublish on' value does not match the expected format of", 'If the default time option is enabled the user can skip the time when scheduling content for unpublication.');
    $this->assertRaw(t('This post is unpublished and will be published @publish_time.', array('@publish_time' => date('Y-m-d H:i:s', strtotime('tomorrow', REQUEST_TIME) + 23400))), 'The user is informed that the content will be published on the requested date, on the default time.');

    // Check that the default time has been added to the scheduler form fields.
    $this->clickLink(t('Edit'));
    $this->assertFieldByName($publish_time_field, date($time_format, strtotime('tomorrow', REQUEST_TIME) + 23400), 'The default time offset has been added to the date field when scheduling content for publication.');
    $this->assertFieldByName($unpublish_time_field, date($time_format, strtotime('tomorrow +1 day', REQUEST_TIME) + 23400), 'The default time offset has been added to the date field when scheduling content for unpublication.');
  }

  /**
   * Test the default time functionality.
   */
  public function testDefaultTime() {
    $this->drupalLogin($this->admin_user);

    foreach (array('textfield', 'date_popup') as $field_type) {
      // Check that the correct default time is added to the scheduled date.
      // For testing we use an offset of 6 hours 30 minutes (23400 seconds).
      $edit = array(
        'scheduler_date_format' => 'Y-m-d H:i:s',
        'scheduler_allow_date_only' => TRUE,
        'scheduler_default_time' => '6:30',
        'scheduler_field_type' => $field_type,
      );
      $this->drupalPost('admin/config/content/scheduler', $edit, t('Save configuration'));
      $this->assertDefaultTime();

      // Check that it is not possible to enter a date format without a time if
      // the 'date only' option is not enabled.
      $edit = array(
        'scheduler_date_format' => 'Y-m-d',
        'scheduler_allow_date_only' => FALSE,
        'scheduler_field_type' => $field_type,
      );
      $this->drupalPost('admin/config/content/scheduler', $edit, t('Save configuration'));
      $this->assertRaw(t('You must either include a time within the date format or enable the date-only option.'), format_string('It is not possible to enter a date format without a time if the "date only" option is not enabled and the field type is set to %field_type.', array('%field_type' => $field_type)));
    }
  }

  /**
   * Tests configuration of different date formats with the Date Popup field.
   */
  public function testDatePopupFormats() {
    $this->drupalLogin($this->admin_user);

    // Define some date formats to test.
    $test_cases = array(
      // By default we are not using the 'date only' option, so passing only a
      // date should fail.
      'Y-m-d' => FALSE,
      'd-m-Y' => FALSE,
      'm-d-Y' => FALSE,
      'n/j/y' => FALSE,
      'd F Y' => FALSE,

      // Test a number of supported date formats.
      'Y-m-d H:i' => TRUE,
      'd-m-Y h:ia' => TRUE,
      'm-d-Y h:iA' => TRUE,
      'n-j-y H:i:s' => TRUE,
      'Y/M/d h:i:sA' => TRUE,
      'j F y h:i:sa' => TRUE,

      // Test a number of date formats with invalid time specifications.
      'y-m-d G:i' => FALSE,
      'y-j-n G:i:sa' => FALSE,
      'Y-m-d g:i:sa' => FALSE,
      'y-m-d g:i:s' => FALSE,
      'n-j-y h:i' => FALSE,
      'd-m-y h:i:s' => FALSE,
      'd/M/y H:i:sA' => FALSE,
      'Y F d H:ia' => FALSE,
    );
    foreach ($test_cases as $date_format => $expected_result) {
      $edit = array(
        'scheduler_date_format' => $date_format,
        'scheduler_field_type' => 'date_popup',
      );
      $this->drupalPost('admin/config/content/scheduler', $edit, t('Save configuration'));
      $message = format_string('When using date popups the date format %format is @expected', array('%format' => $date_format, '@expected' => $expected_result ? 'allowed' : 'not allowed.'));
      $assert = $expected_result ? 'assertNoRaw' : 'assertRaw';
      $this->$assert('Error message', $message);
    }
  }
}
