<?php

/**
 * @file
 * Automated tests for the Taxonomy Access Control module.
 */

/**
 * Provides a base test class and helper methods for automated tests.
 */
class TaxonomyAccessTestCase extends DrupalWebTestCase {
  // There are four types of users:
  // site admins, taxonomy admins, content editors, and regular users.
  protected $users = array();
  protected $user_roles = array();
  protected $user_config = array(
    'site_admin' => array(
      'access content',
      'access site reports',
      'access administration pages',
      'administer permissions',
      'create article content',
      'edit any article content',
      'create page content',
      'edit any page content',
    ),
    'tax_admin' => array(
      'access content',
      'administer taxonomy',
    ),
    'editor' => array(
      'access content',
      'create article content',
      'create page content',
    ),
    'regular_user' =>
      array(
        'access content',
      ),
  );

  public function setUp() {
    // Enable module and dependencies.
    parent::setUp('taxonomy_access');

    // Rebuild node access on installation.
    node_access_rebuild();

    // Configure users with base permission patterns.
    foreach ($this->user_config as $user => $permissions) {
      $this->users[$user] = $this->drupalCreateUser($permissions);

      // Save the role ID separately so it's easy to retrieve.
      foreach ($this->users[$user]->roles as $rid => $role) {
        if ($rid != DRUPAL_AUTHENTICATED_RID) {
          $this->user_roles[$user] = user_role_load($rid);
        }
      }
    }

    // Give the anonymous and authenticated roles ignore grants.
    $rows = array();
    foreach (array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID) as $rid) {
      $ignore = array(
        'view' => TAXONOMY_ACCESS_NODE_IGNORE,
        'update' => TAXONOMY_ACCESS_NODE_IGNORE,
        'delete' => TAXONOMY_ACCESS_NODE_IGNORE,
      );
      $rows[] = _taxonomy_access_format_grant_record(TAXONOMY_ACCESS_GLOBAL_DEFAULT, $rid, $ignore, TRUE);
    }
    taxonomy_access_set_default_grants($rows);

    foreach (array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID) as $rid) {
      $r =
        db_query(
          'SELECT grant_view FROM {taxonomy_access_default}
           WHERE vid = :vid AND rid = :rid',
          array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT, ':rid' => $rid)
        )
        ->fetchField();
      $this->assertTrue(is_numeric($r) && $r == 0, t("Set global default for role %rid to <em>Ignore</em>", array('%rid' => $rid)));
    }
  }

  /**
   * Creates a vocabulary with a certain name.
   *
   * @param string $machine_name
   *   A machine-safe name.
   *
   * @return object
   *   The vocabulary object.
   */
  function createVocab($machine_name) {
    $vocabulary = new stdClass();
    $vocabulary->name = $machine_name;
    $vocabulary->description = $this->randomName();
    $vocabulary->machine_name = $machine_name;
    $vocabulary->help = '';
    $vocabulary->weight = mt_rand(0, 10);
    taxonomy_vocabulary_save($vocabulary);
    return $vocabulary;
  }

  /**
   * Creates a new term in the specified vocabulary.
   *
   * @param string $machine_name
   *   A machine-safe name.
   * @param object $vocab
   *   A vocabulary object.
   * @param int|null $parent
   *   (optional) The tid of the parent term, if any.  Defaults to NULL.
   *
   * @return object
   *   The taxonomy term object.
   */
  function createTerm($machine_name, $vocab, $parent = NULL) {
    $term = new stdClass();
    $term->name = $machine_name;
    $term->description = $machine_name;
    // Use the first available text format.
    $term->format =
      db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField();
    $term->vid = $vocab->vid;
    $term->vocabulary_machine_name = $vocab->machine_name;
    if (!is_null($parent)) {
      $term->parent = $parent;
    }
    taxonomy_term_save($term);
    return $term;
  }

  /**
   * Creates a taxonomy field and adds it to the page content type.
   *
   * @param string $machine_name
   *   The machine name of the vocabulary to use.
   * @param string $widget
   *   (optional) The name of the widget to use.  Defaults to 'options_select'.
   * @param int $count
   *   (optional) The allowed number of values.  Defaults to unlimited.
   *
   * @return array
   *   Array of instance data.
   */
  function createField($machine_name, $widget = 'options_select', $count = FIELD_CARDINALITY_UNLIMITED) {
    $field = array(
      'field_name' => $machine_name,
      'type' => 'taxonomy_term_reference',
      'cardinality' => $count,
      'settings' => array(
        'allowed_values' => array(
          array(
            'vocabulary' => $machine_name,
            'parent' => 0,
          ),
        ),
      ),
    );
    $field = field_create_field($field);

    $instance = array(
      'field_name' => $machine_name,
      'bundle' => 'page',
      'entity_type' => 'node',
      'widget' => array(
        'type' => $widget,
      ),
      'display' => array(
        'default' => array(
          'type' => 'taxonomy_term_reference_link',
        ),
      ),
    );

    return field_create_instance($instance);
  }

 /**
   * Creates an article with the specified terms.
   *
   * @param array $autocreate
   *   (optional) An array of term names to autocreate. Defaults to array().
   * @param array $existing
   *   (optional) An array of existing term IDs to add.
   *
   * @return object
   *   The node object.
   */
  function createArticle($autocreate = array(), $existing = array()) {
    $values = array();
    foreach ($autocreate as $name) {
      $values[] = array('tid' => 'autocreate', 'vid' => 1, 'name' => $name, 'vocabulary_machine_name' => 'tags');
    }
    foreach ($existing as $tid) {
      $values[] = array('tid' => $tid, 'vid' => 1, 'vocabulary_machine_name' => 'tags');
    }

    // Bloody $langcodes.
    $values = array(LANGUAGE_NONE => $values);

    $settings = array(
      'type' => 'article',
      'field_tags' => $values,
    );

    return $this->drupalCreateNode($settings);
  }

  /**
   * Submits the node access rebuild form.
   */
  function rebuild() {
    $this->drupalPost('admin/reports/status/rebuild', array(), t('Rebuild permissions'));
    $this->assertText(t('The content access permissions have been rebuilt.'));
  }

  /**
   * Asserts that a status column and "Configure" link is found for the role.
   *
   * @param array $statuses
   *   An associative array of role statuses, keyed by role ID. Each item
   *   should be TRUE if the role is enabled, and FALSE otherwise.
   */
  function checkRoleConfig(array $statuses) {
    $roles = _taxonomy_access_user_roles();

    // Log in as the administrator.
    $this->drupalLogout();
    $this->drupalLogin($this->users['site_admin']);
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG);

    foreach ($statuses as $rid => $status) {
      // Assert that a "Configure" link is available for the role.
      $this->assertLinkByHref(
        TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit",
        0,
        t('"Configure" link is available for role %rid.', array('%rid' => $rid)));
    }

    // Retrieve the grant status table.
    $shown = array();
    $table = $this->xpath('//table/tbody');
    $table = reset($table);
    // SimpleXML has fake arrays so we have to do this to get the data out.
    foreach ($table->tr as $row) {
      $tds = array();
      foreach ($row->td as $value) {
        $tds[] = (string) $value;
      }
      $shown[$tds[0]] = $tds[1];
    }

    foreach ($statuses as $rid => $status) {
      // Assert that the form shows the passed status.
      if ($status) {
        $this->assertTrue(
          $shown[$roles[$rid]] == t('Enabled'),
          format_string('Role %role is enabled.', array('%role' => $rid)));
      }
      else {
        $this->assertTrue(
          $shown[$roles[$rid]] == t('Disabled'),
          format_string('Role %role is disabled.', array('%role' => $rid)));
      }

      // Assert that a "Configure" link is available for the role.
      $this->assertLinkByHref(
        TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit",
        0,
        t('"Configure" link is available for role %rid.',
          array('%rid' => $rid)));
    }

  }

  /**
   * Asserts that an enable link is or is not found for the role.
   *
   * @param int $rid
   *   The role ID to check.
   * @param bool $found
   *   Whether the link should be found, or not.
   */
  function checkRoleEnableLink($rid, $found) {
    if ($found) {
      $this->assertLinkByHref(
        TAXONOMY_ACCESS_CONFIG . "/role/$rid/enable",
        0,
        t('Enable link is available for role %rid.', array('%rid' => $rid))
      );
    }
    else {
      $this->assertNoLinkByHref(
        TAXONOMY_ACCESS_CONFIG . "/role/$rid/enable",
        t('Enable link is not available for role %rid.', array('%rid' => $rid))
      );
    }
  }

  /**
   * Asserts that a disable link is or is not found for the role.
   *
   * @param int $rid
   *   The role ID to check.
   * @param bool $found
   *   Whether the link should be found, or not.
   */
  function checkRoleDisableLink($rid, $found) {
    if ($found) {
      $this->assertLinkByHref(
        TAXONOMY_ACCESS_CONFIG . "/role/$rid/delete",
        0,
        t('Disable link is available for role %rid.', array('%rid' => $rid))
      );
    }
    else {
      $this->assertNoLinkByHref(
        TAXONOMY_ACCESS_CONFIG . "/role/$rid/delete",
        t('Disable link is not available for role %rid.', array('%rid' => $rid))
      );
    }
  }

  /**
   * Adds a term row on the role configuration form.
   *
   * @param array &$edit
   *   The form data to post.
   * @param int $vid
   *   (optional) The vocabulary ID. Defaults to
   *   TAXONOMY_ACCESS_GLOBAL_DEFAULT.
   * @param $int tid
   *   (optional) The term ID. Defaults to TAXONOMY_ACCESS_VOCABULARY_DEFAULT.
   * @param int $view
   *   (optional) The view grant value. Defaults to
   *    TAXONOMY_ACCESS_NODE_IGNORE.
   * @param int $update
   *   (optional) The update grant value. Defaults to
   * @param int $delete
   *   (optional) The delete grant value. Defaults to
   *   TAXONOMY_ACCESS_NODE_IGNORE.
   * @param int $create
   *   (optional) The create grant value. Defaults to
   *   TAXONOMY_ACCESS_TERM_DENY.
   * @param int $list
   *   (optional) The list grant value. Defaults to TAXONOMY_ACCESS_TERM_DENY.
   */
  function addFormRow(&$edit,  $vid = TAXONOMY_ACCESS_GLOBAL_DEFAULT, $tid = TAXONOMY_ACCESS_VOCABULARY_DEFAULT, $view = TAXONOMY_ACCESS_NODE_IGNORE, $update = TAXONOMY_ACCESS_NODE_IGNORE, $delete = TAXONOMY_ACCESS_NODE_IGNORE, $create = TAXONOMY_ACCESS_TERM_DENY, $list = TAXONOMY_ACCESS_TERM_DENY) {
    $new_value = $tid ? "term $tid" : "default $vid";
    $edit["new[$vid][item]"] = $new_value;
    $edit["new[$vid][grants][$vid][0][view]"] = $view;
    $edit["new[$vid][grants][$vid][0][update]"] = $update;
    $edit["new[$vid][grants][$vid][0][delete]"] = $delete;
    $edit["new[$vid][grants][$vid][0][create]"] = $create;
    $edit["new[$vid][grants][$vid][0][list]"] = $list;
  }

  /**
   * Configures a row on the TAC configuration form.
   *
   * @param array &$edit
   *   The form data to post.
   * @param int $vid
   *   (optional) The vocabulary ID. Defaults to
   *   TAXONOMY_ACCESS_GLOBAL_DEFAULT.
   * @param $int tid
   *   (optional) The term ID. Defaults to TAXONOMY_ACCESS_VOCABULARY_DEFAULT.
   * @param int $view
   *   (optional) The view grant value. Defaults to
   *    TAXONOMY_ACCESS_NODE_IGNORE.
   * @param int $update
   *   (optional) The update grant value. Defaults to
   * @param int $delete
   *   (optional) The delete grant value. Defaults to
   *   TAXONOMY_ACCESS_NODE_IGNORE.
   * @param int $create
   *   (optional) The create grant value. Defaults to
   *   TAXONOMY_ACCESS_TERM_DENY.
   * @param int $list
   *   (optional) The list grant value. Defaults to TAXONOMY_ACCESS_TERM_DENY.
   */
  function configureFormRow(&$edit,  $vid = TAXONOMY_ACCESS_GLOBAL_DEFAULT, $tid = TAXONOMY_ACCESS_VOCABULARY_DEFAULT, $view = TAXONOMY_ACCESS_NODE_IGNORE, $update = TAXONOMY_ACCESS_NODE_IGNORE, $delete = TAXONOMY_ACCESS_NODE_IGNORE, $create = TAXONOMY_ACCESS_TERM_DENY, $list = TAXONOMY_ACCESS_TERM_DENY) {
    $edit["grants[$vid][$tid][view]"] = $view;
    $edit["grants[$vid][$tid][update]"] = $update;
    $edit["grants[$vid][$tid][delete]"] = $delete;
    $edit["grants[$vid][$tid][create]"] = $create;
    $edit["grants[$vid][$tid][list]"] = $list;
  }
}

/**
 * Tests the module's response to changes from other modules.
 */
class TaxonomyAccessExternalChanges extends TaxonomyAccessTestCase {
  public static function getInfo() {
    return array(
      'name' => 'External changes',
      'description' => "Test the module's response to changes from other modules.",
      'group' => 'Taxonomy Access Control',
    );
  }

  public function setUp() {
    parent::setUp();
  }

  /*
1. delete a term
2. delete a role
3. delete a field attachment
4. modify a field attachment
5. delete a vocabulary
6. add terms to node
7. remove terms from node
  */
}

/**
 * Tests the module's configuration forms.
 */
class TaxonomyAccessConfigTest extends TaxonomyAccessTestCase {
  protected $articles = array();
  protected $pages = array();
  protected $vocabs = array();
  protected $terms = array();

  public static function getInfo() {
    return array(
      'name' => 'Configuration forms',
      'description' => 'Test module configuration forms.',
      'group' => 'Taxonomy Access Control',
    );
  }

  public function setUp() {
    parent::setUp();

    // Add two taxonomy fields to pages.
    foreach (array('v1', 'v2') as $vocab) {
      $this->vocabs[$vocab] = $this->createVocab($vocab);
      $this->createField($vocab);
      $this->terms[$vocab . 't1'] =
        $this->createTerm($vocab . 't1', $this->vocabs[$vocab]);
      $this->terms[$vocab . 't2'] =
        $this->createTerm($vocab . 't2', $this->vocabs[$vocab]);
    }

    // Set up a variety of nodes with different term combinations.
    $this->articles['no_tags'] = $this->createArticle();
    $this->articles['one_tag'] =
      $this->createArticle(array($this->randomName()));
    $this->articles['two_tags'] =
      $this->createArticle(array($this->randomName(), $this->randomName()));

    $this->pages['no_tags'] = $this->createPage();
    foreach ($this->terms as $t1) {
      $this->pages[$t1->name] = $this->createPage(array($t1->name));
      foreach ($this->terms as $t2) {
        $this->pages[$t1->name . '_' . $t2->name] =
          $this->createPage(array($t1->name, $t2->name));
      }
    }
  }

  /**
   * Creates a page with the specified terms.
   *
   * @param array $terms
   *   (optional) An array of term names to tag the page.  Defaults to array().
   *
   * @return object
   *   The node object.
   */
  function createPage($tags = array()) {
    $v1 = array();
    $v2 = array();

    foreach ($tags as $name) {
      switch ($this->terms[$name]->vid) {
        case ($this->vocabs['v1']->vid):
          $v1[] = array('tid' => $this->terms[$name]->tid);
          break;

        case ($this->vocabs['v2']->vid):
          $v2[] = array('tid' => $this->terms[$name]->tid);
          break;
      }
    }

    // Bloody $langcodes.
    $v1 = array(LANGUAGE_NONE => $v1);
    $v2 = array(LANGUAGE_NONE => $v2);

    $settings = array(
      'type' => 'page',
      'v1' => $v1,
      'v2' => $v2,
    );

    return $this->drupalCreateNode($settings);
  }

/*
@todo
- check anon and auth forms
- add recursive for vocab and for term
- change multiple
- delete multiple
- configure create and list
 */

  /**
   * Tests the initial state of the test environment.
   *
   * Verifies that:
   * - Access to all nodes is denied for anonymous users.
   * - The main admin page provides the correct configuration links.
   */
  public function testSetUpCheck() {
    // Visit all nodes as anonymous and verify that access is denied.
    foreach ($this->articles as $key => $article) {
      $this->drupalGet('node/' . $article->nid);
      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
    }
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);
      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
    }

    // Log in as the regular_user.
    $this->drupalLogin($this->users['regular_user']);

    // Visit all nodes and verify that access is denied.
    foreach ($this->articles as $key => $article) {
      $this->drupalGet('node/' . $article->nid);
      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
    }
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);
      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
    }

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Confirm that only edit links are available for anon. and auth.
    $this->checkRoleConfig(array(
      DRUPAL_ANONYMOUS_RID => TRUE,
      DRUPAL_AUTHENTICATED_RID => TRUE,
    ));
  }

  /**
   * Tests configuring a global default.
   *
   * Verifies that:
   * - Access is updated for all nodes when there are no other configurations.
   * - Access is updated for the correct nodes when there are specific term
   *    and vocabulary configurations.
   */
  public function testGlobalDefaultConfig() {
    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the admin form to give anonymous view allow in the global default.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $this->configureFormRow($edit, TAXONOMY_ACCESS_GLOBAL_DEFAULT, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
    $this->drupalPost(NULL, $edit, 'Save all');

    // Log out.
    $this->drupalLogout();

    // Visit each node and verify that access is allowed.
    foreach ($this->articles as $key => $article) {
      $this->drupalGet('node/' . $article->nid);
      $this->assertResponse(200, t("Access to %name article (nid %nid) is allowed.", array('%name' => $key, '%nid' => $article->nid)));
    }
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);
      $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
    }

    // Add some specific configurations programmatically.

    // Set the v1 default to view allow.
    $default_config = _taxonomy_access_format_grant_record(
      $this->vocabs['v1']->vid, DRUPAL_ANONYMOUS_RID, array('view' => TAXONOMY_ACCESS_NODE_ALLOW), TRUE
    );
    taxonomy_access_set_default_grants(array($default_config));

    // Set v1t1 and v2t1 to view allow.
    $term_configs = array();
    foreach (array('v1t1', 'v2t1') as $name) {
      $term_configs[] = _taxonomy_access_format_grant_record(
        $this->terms[$name]->vid, DRUPAL_ANONYMOUS_RID, array('view' => TAXONOMY_ACCESS_NODE_ALLOW)
      );
    }
    taxonomy_access_set_term_grants($term_configs);

    // This leaves articles and the v2t2 page controlled by the global default.

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the admin form to give anonymous view deny in the global default.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $this->configureFormRow($edit, TAXONOMY_ACCESS_GLOBAL_DEFAULT, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_DENY);
    $this->drupalPost(NULL, $edit, 'Save all');

    // Log out.
    $this->drupalLogout();

    // Visit each artile and verify that access is denied.
    foreach ($this->articles as $key => $article) {
      $this->drupalGet('node/' . $article->nid);
      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
    }

    // Visit each page.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      switch (TRUE) {
        // If the page has no tags, access should be denied.
        case ($key == 'no_tags'):
        // If the page is tagged with v2t2, access should be denied.
        case (strpos($key, 'v2t2') !== FALSE):
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Otherwise, access should be allowed.
        default:
          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
          break;
      }
    }
  }

  /**
   * Tests configuring vocabulary defaults.
   *
   * Verifies that:
   * - Access is updated correctly when the vocabulary default is added and
   *   configured.
   * - Access is updated correctly when there is a specific term configuration
   *   in the vocabulary.
   * - Access is updated correctly when multiple defaults are changed.
   * - Access is updated correctly when the vocabulary default is deleted.
   */
  public function testVocabularyDefaultConfig() {
    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Enable the vocabulary.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    // @todo
    //   - Ensure that all vocabularies are options in the "Add" fieldset.
    $edit = array();
    $edit['enable_vocab'] = $this->vocabs['v1']->vid;
    $this->drupalPost(NULL, $edit, t('Add'));

    // @todo
    //   - Ensure that the vocabulary is removed from the "Add" fieldset.
    //   - Ensure that the fieldset for the vocabulary appears.
    //   - Ensure that no other fieldsets or rows appear.

    // Give anonymous view allow for the v1 default.
    $edit = array();
    $this->configureFormRow($edit, $this->vocabs['v1']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
    $this->drupalPost(NULL, $edit, 'Save all');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      // If the page is tagged with a v1 term, access should be allowed.
      if (strpos($key, 'v1') !== FALSE) {
        $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
      }
      // Otherwise, access should be denied.
      else {
        $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
      }
    }

    // Programmatically enable v2 and add a specific configuration for v2t1.
    taxonomy_access_enable_vocab($this->vocabs['v2']->vid, DRUPAL_ANONYMOUS_RID);
    $term_config = _taxonomy_access_format_grant_record(
      $this->terms['v2t1']->tid, DRUPAL_ANONYMOUS_RID, array('view' => TAXONOMY_ACCESS_NODE_IGNORE)
    );
    taxonomy_access_set_term_grants(array($term_config));

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the admin form to give anonymous view deny for the v2 default.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $this->configureFormRow($edit, $this->vocabs['v2']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_DENY);
    $this->drupalPost(NULL, $edit, 'Save all');

    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');

    // Log out.
    $this->drupalLogout();
    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      switch (TRUE) {
        // If the page is tagged with v2t2, the v2 default is inherited: Deny.
        case (strpos($key, 'v2t2') !== FALSE):
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Otherwise, if the page is tagged with v1, it's allowed.
        case (strpos($key, 'v1') !== FALSE):
          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Access should be denied by default.
        default:
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;
      }
    }

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the form to change the configuration: Allow for v2; Deny for v1.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $this->configureFormRow($edit, $this->vocabs['v2']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
    $this->configureFormRow($edit, $this->vocabs['v1']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_DENY);
    $this->drupalPost(NULL, $edit, 'Save all');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      switch (TRUE) {
        // If the page is tagged with a v1 term, access should be denied.
        case (strpos($key, 'v1') !== FALSE):
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Otherwise, if the page is tagged with v2t2, the default is
        // inherited and access should be allowed.
        case (strpos($key, 'v2t2') !== FALSE):
          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Access should be denied by default.
        default:
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;
      }
    }

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the admin form to disable v1.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $this->clickLink(t('delete all v1 access rules'));
    $this->assertText("Are you sure you want to delete all Taxonomy access rules for v1", t('Disable form for vocabulary loaded.'));
    $this->drupalPost(NULL, array(), 'Delete all');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      // If the page is tagged with v2t2, access should be allowed.
      if (strpos($key, 'v2t2') !== FALSE) {
        $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
      }
      // Otherwise, access should be denied.
      else {
        $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
      }
    }
  }

  /**
   * Tests configuring specific terms.
   *
   * Verifies that:
   * - Access is updated correctly when the term configuration is added.
   * - Access is updated correctly when there is a vocabulary default.
   * - Access is updated correctly when multiple configurations are changed.
   * - Access is updated correctly when the term configuration is deleted.
   */
  public function testTermConfig() {
    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the admin form to enable v1 and give anonymous view allow for v1t1.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $edit['enable_vocab'] = $this->vocabs['v1']->vid;
    $this->drupalPost(NULL, $edit, t('Add'));
    $edit = array();
    $this->addFormRow($edit, $this->vocabs['v1']->vid, $this->terms['v1t1']->tid, TAXONOMY_ACCESS_NODE_ALLOW);
    $this->drupalPost(NULL, $edit, 'Add');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      // If the page is tagged with v1t1, access should be allowed.
      if (strpos($key, 'v1t1') !== FALSE) {
        $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
      }
      // Otherwise, access should be denied.
      else {
        $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
      }
    }

    // Enable v2 programmatically.
    taxonomy_access_enable_vocab($this->vocabs['v2']->vid, DRUPAL_ANONYMOUS_RID);

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the admin form to give anonymous view deny for v2t1.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $this->addFormRow($edit, $this->vocabs['v2']->vid, $this->terms['v2t1']->tid, TAXONOMY_ACCESS_NODE_DENY);
    $this->drupalPost(NULL, $edit, 'Add');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      switch (TRUE) {
        // If the page is tagged with v2t1, access should be denied.
        case (strpos($key, 'v2t1') !== FALSE):
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Otherwise, if the page is tagged with v1t1, it's allowed.
        case (strpos($key, 'v1t1') !== FALSE):
          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Access should be denied by default.
        default:
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;
      }
    }

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the form to change the configuration: Allow for v2t1; Deny for v1t1.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $this->configureFormRow(
      $edit, $this->vocabs['v2']->vid, $this->terms['v2t1']->tid, TAXONOMY_ACCESS_NODE_ALLOW
    );
    $this->configureFormRow(
      $edit, $this->vocabs['v1']->vid, $this->terms['v1t1']->tid, TAXONOMY_ACCESS_NODE_DENY
    );
    $this->drupalPost(NULL, $edit, 'Save all');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      switch (TRUE) {
        // If the page is tagged with v1t1, access should be denied.
        case (strpos($key, 'v1t1') !== FALSE):
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Otherwise, if the page is tagged with v2t1, it's allowed.
        case (strpos($key, 'v2t1') !== FALSE):
          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
          break;

        // Access should be denied by default.
        default:
          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
          break;
      }
    }

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Use the form to delete the v2t1 configuration.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $edit["grants[{$this->vocabs['v2']->vid}][{$this->terms['v2t1']->tid}][remove]"] = 1;
    $this->drupalPost(NULL, $edit, 'Delete selected');

    // Log out.
    $this->drupalLogout();

    // Visit each page and verify whether access is allowed or denied.
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);

      // Access to all pages should be denied.
      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
    }
  }

  /**
   * Tests adding a term configuration with children.
   *
   * @todo
   *   Check that node access is updated for these as well.
   */
  public function testTermWithChildren() {
    // Create some additional taxonomy terms in a hierarchy:
    // v1
    // - v1t1
    // - - v1t1c1
    // - - - v1t1c1g1
    // - - - v1t1c1g2
    // - - v1t1c2
    // - - v1t2

    $this->terms['v1t1c1'] = $this->createTerm(
      'v1t1c1',
      $this->vocabs['v1'],
      $this->terms['v1t1']->tid
    );
    $this->terms['v1t1c2'] = $this->createTerm(
      'v1t1c2',
      $this->vocabs['v1'],
      $this->terms['v1t1']->tid
    );
    $this->terms['v1t1c1g1'] = $this->createTerm(
      'v1t1c1g1',
      $this->vocabs['v1'],
      $this->terms['v1t1c1']->tid
    );
    $this->terms['v1t1c1g2'] = $this->createTerm(
      'v1t1c1g2',
      $this->vocabs['v1'],
      $this->terms['v1t1c1']->tid
    );

    // Add pages tagged with each.
    foreach (array('v1t1c1', 'v1t1c2', 'v1t1c1g1', 'v1t1c1g2') as $name) {
      $this->pages[$name] = $this->createPage(array($name));
    }

    // Log in as the administrator.
    $this->drupalLogin($this->users['site_admin']);

    // Enable v1 programmatically.
    taxonomy_access_enable_vocab($this->vocabs['v1']->vid, DRUPAL_ANONYMOUS_RID);
    // Use the admin form to give anonymous view allow for v1t1 and children.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
    $edit = array();
    $edit["new[{$this->vocabs['v1']->vid}][recursive]"] = 1;
    $this->addFormRow($edit, $this->vocabs['v1']->vid, $this->terms['v1t1']->tid, TAXONOMY_ACCESS_NODE_ALLOW);
    $this->drupalPost(NULL, $edit, 'Add');

  }

  /**
   * Tests enabling and disabling TAC for a custom role.
   */
  public function testRoleEnableDisable() {
    // Save some typing.
    $rid = $this->user_roles['regular_user']->rid;
    $name = $this->user_roles['regular_user']->name;

    // Check that the role is disabled by default.
    $this->checkRoleConfig(array(
      DRUPAL_ANONYMOUS_RID => TRUE,
      DRUPAL_AUTHENTICATED_RID => TRUE,
      $rid => FALSE,
    ));

    // Test enabling the role.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit");

    // Check that there is:
    // - An enable link
    // - No disable link
    // @todo
    //   - No grant tables.
    $this->checkRoleEnableLink($rid, TRUE);
    $this->checkRoleDisableLink($rid, FALSE);

    // Enable the role and check that there is:
    // - A disable link
    // - No enable link
    // @todo
    //   - A global default table (with correct values?)
    //   - An "Add vocabulary" fieldset.
    //   - No vocabulary fieldsets or term data.
    $this->clickLink(format_string('Enable @name', array('@name' => $name)));
    $this->checkRoleEnableLink($rid, FALSE);
    $this->checkRoleDisableLink($rid, TRUE);

    // Update the global default to allow view.
    $edit = array();
    $this->configureFormRow($edit, TAXONOMY_ACCESS_GLOBAL_DEFAULT, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
    $this->drupalPost(NULL, $edit, 'Save all');

    // Confirm that all three roles are enabled.
    $this->checkRoleConfig(array(
      DRUPAL_ANONYMOUS_RID => TRUE,
      DRUPAL_AUTHENTICATED_RID => TRUE,
      $rid => TRUE,
    ));

    // Check that the role is configured.
    $r =
      db_query(
        'SELECT grant_view FROM {taxonomy_access_default}
         WHERE vid = :vid AND rid = :rid',
        array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT, ':rid' => $rid)
      )
      ->fetchField();
    $this->assertTrue($r == TAXONOMY_ACCESS_NODE_ALLOW, t('Used form to grant the role %role view in the global default.', array('%role' => $name)));

    // Log in as the regular_user.
    $this->drupalLogout();
    $this->drupalLogin($this->users['regular_user']);

    // Visit each node and verify that access is allowed.
    foreach ($this->articles as $key => $article) {
      $this->drupalGet('node/' . $article->nid);
      $this->assertResponse(200, t("Access to %name article (nid %nid) is allowed.", array('%name' => $key, '%nid' => $article->nid)));
    }
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);
      $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
    }

    // Log in as the administrator.
    $this->drupalLogout();
    $this->drupalLogin($this->users['site_admin']);

    // Test disabling the role.
    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit");
    $this->clickLink(t('Disable @name', array('@name' => $name)));
    $this->assertText("Are you sure you want to delete all taxonomy access rules for the role $name", t('Disable form for role loaded.'));
    $this->drupalPost(NULL, array(), 'Delete all');

    // Confirm that a confirmation message appears.
    $this->assertText("All taxonomy access rules deleted for role $name", t('Confirmation message found.'));

    // Check that there is:
    // - An enable link
    // - No disable link
    // @todo
    //   - No grant tables.
    $this->checkRoleEnableLink($rid, TRUE);
    $this->checkRoleDisableLink($rid, FALSE);

    // Confirm edit/enable/disable links are in their original state.
    $this->checkRoleConfig(array(
      DRUPAL_ANONYMOUS_RID => TRUE,
      DRUPAL_AUTHENTICATED_RID => TRUE,
      $rid => FALSE,
    ));

    // Check that the role is no longer configured.
    $r =
      db_query(
        'SELECT grant_view FROM {taxonomy_access_default}
         WHERE rid = :rid',
        array(':rid' => $rid)
      )
      ->fetchAll();
    $this->assertTrue(empty($r), t('All records removed for role %role.', array('%role' => $name)));

    // @todo
    //   - Add a term configuration and make sure that gets deleted too.

    // Log in as the regular_user.
    $this->drupalLogout();
    $this->drupalLogin($this->users['regular_user']);

    // Visit all nodes and verify that access is again denied.
    foreach ($this->articles as $key => $article) {
      $this->drupalGet('node/' . $article->nid);
      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
    }
    foreach ($this->pages as $key => $page) {
      $this->drupalGet('node/' . $page->nid);
      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
    }
  }
}

/**
 * Tests node access for all possible grant combinations.
 */
class TaxonomyAccessNodeGrantTest extends TaxonomyAccessTestCase {

  // There are three roles for node access testing:
  // global_allow   Receives "Allow" in the global default.
  // global_ignore  Receives "Ignore" in the global default.
  // global_deny    Receives "Deny" in the global default.
  // All roles receive the same permissions for terms and vocab defaults.
  protected $roles = array();
  protected $role_config = array(
    'global_allow' => array(),
    'global_ignore' => array(),
    'global_deny' => array(),
  );

  protected $vocabs = array();

  public static function getInfo() {
    return array(
      'name' => 'Node access',
      'description' => 'Test node access for various grant configurations.',
      'group' => 'Taxonomy Access Control',
    );
  }

  public function setUp() {
    parent::setUp();

    // Configure roles with no additional permissions.
    foreach ($this->role_config as $role_name => $permissions) {
      $this->roles[$role_name] = $this->drupalCreateRole(array(), $role_name);
    }

    $node_grants = array('view', 'update', 'delete');

    // Set up our testing taxonomy.

    // We will create 4 vocabularies: a, i, d, and nc
    // These names indicate what grant the vocab. default will have for view.
    // (NC means the vocab default is not configured.)

    $grant_types = array(
      'a' => array(),
      'i' => array(),
      'd' => array(),
      'nc' => array(),
    );

    // View alone can be used to test V/U/D because the logic is identical.
    foreach ($node_grants as $grant) {
      $grant_types['a'][$grant] = TAXONOMY_ACCESS_NODE_ALLOW;
      $grant_types['i'][$grant] = TAXONOMY_ACCESS_NODE_IGNORE;
      $grant_types['d'][$grant] = TAXONOMY_ACCESS_NODE_DENY;
    }

    // Each vocabulary will have four parent terms in the same fashion:
    // a_parent, i_parent, d_parent, and nc_parent.

    // Each of these_parent terms will have children in each class, as well:
    // a_child, i_child, d_child, and nc_child.

    // So, each vocab looks something like:
    // - a_parent
    // - - a_child
    // - - i_child
    // - - d_child
    // - - nc_child
    // - i_parent
    // - - a_child
    // - - i_child
    // - - d_child
    // - - nc_child
    // - d_parent
    // - - a_child
    // - - i_child
    // - - d_child
    // - - nc_child
    // - nc_parent
    // - - a_child
    // - - i_child
    // - - d_child
    // - - nc_child

    $term_rows = array();
    $default_rows = array();
    $this->setUpAssertions = array();

    // Configure terms, vocabularies, and grants.
    foreach ($grant_types as $vocab_name => $default_grants) {
      // Create the vocabulary.
      $vocab_name = "v" . $vocab_name;
      $this->vocabs[$vocab_name] = array();
      $this->vocabs[$vocab_name]['vocab'] = parent::createVocab($vocab_name);
      $this->vocabs[$vocab_name]['terms'] = array();
      $vocab = $this->vocabs[$vocab_name]['vocab'];

      // Add a field for the vocabulary to pages.
      $this->createField($vocab_name);

      // Configure default grants for the vocabulary for each role.
      if (!empty($default_grants)) {
        foreach ($this->roles as $name => $role) {
          $default_rows[] =  _taxonomy_access_format_grant_record($vocab->vid, $role, $default_grants, TRUE);
          $this->setUpAssertions[] = array(
            'grant' => $default_grants['view'],
            'query' => 'SELECT grant_view FROM {taxonomy_access_default} WHERE vid = :vid AND rid = :rid',
            'args' => array(':vid' => $vocab->vid, ':rid' => $role),
            'message' => t('Configured default grants for vocab %vocab, role %role', array('%vocab' => $vocab->machine_name, '%role' => $name)),
          );
        }
      }

      // Create terms.
      foreach ($grant_types as $parent_name => $parent_grants) {

        // Create parent term.
        $parent_name = $vocab_name . "__" . $parent_name . "_parent";
        $this->vocabs[$vocab_name]['terms'][$parent_name] =
          parent::createTerm($parent_name, $vocab);
        $parent_id = $this->vocabs[$vocab_name]['terms'][$parent_name]->tid;

        // Configure grants for the parent term for each role.
        if (!empty($parent_grants)) {
          foreach ($this->roles as $name => $role) {
            $term_rows[] =  _taxonomy_access_format_grant_record($parent_id, $role, $parent_grants);
            $this->setUpAssertions[] = array(
              'grant' => $parent_grants['view'],
              'query' => 'SELECT grant_view FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
              'args' => array(':tid' => $parent_id, ':rid' => $role),
              'message' => t('Configured grants for term %term, role %role', array('%term' => $parent_name, '%role' => $name)),
            );
          }
        }

        // Create child terms.
        foreach ($grant_types as $child_name => $child_grants) {
          $child_name = $parent_name . "__" . $child_name . "_child";
          $this->vocabs[$vocab_name]['terms'][$child_name] =
            parent::createTerm($child_name, $vocab, $parent_id);
          $child_id = $this->vocabs[$vocab_name]['terms'][$child_name]->tid;

          // Configure grants for the child term for each role.
          if (!empty($child_grants)) {
            foreach ($this->roles as $name => $role) {
              $term_rows[] =  _taxonomy_access_format_grant_record($child_id, $role, $child_grants);
              $this->setUpAssertions[] = array(
                'grant' => $child_grants['view'],
                'query' => 'SELECT grant_view FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
                'args' => array(':tid' => $child_id, ':rid' => $role),
                'message' => t('Configured grants for term %term, role %role', array('%term' => $child_name, '%role' => $name)),
              );
            }
          }
        }
      }
    }

    // Set the grants.
    taxonomy_access_set_default_grants($default_rows);
    taxonomy_access_set_term_grants($term_rows);
  }

  /**
   * Verifies that all grants were properly stored during setup.
   */
  public function testSetUpCheck() {
    // Check that all records were properly stored.
    foreach ($this->setUpAssertions as $assertion) {
      $r = db_query($assertion['query'], $assertion['args'])->fetchField();
      $this->assertTrue(
        (is_numeric($r) && $r == $assertion['grant']),
        $assertion['message']
      );
    }
  }

  // Role config tests:
  // Create a role
  // Create a user with the role
  // Configure role grants via form
  // Add, with children, delete
  // Confirm records stored
  // Confirm node access properly updated
  // Go back and edit, repeat.
  // Disable role.
  // Confirm form.
  // Update node access if prompted.
  // Confirm records deleted.
  // Confirm node access updated.

  // 1. delete a term
  // 2. change a grant config
  // 3. delete a grant config
  // 4. change a vocab default
  // 5. delete a voacb default
  // 6. disable a role
  // 7. delete a role
  // 8. delete a field attachment
  // 9. delete a vocabulary
}

/**
 * Tests term grants for all possible grant combinations.
 */
class TaxonomyAccessTermGrantTest extends TaxonomyAccessTestCase {
  // There are four roles for term access testing:
  // ctlt   Receives both "Create" and "List" in the global default.
  // ctlf   Receives "Create" but not "List" in the global default.
  // cflt   Receives "List" but not "Create" in the global default.
  // cflf   Receives neither "Create" nor "List" in the global default.
  // All roles receive the same permissions for terms and vocab defaults.
  protected $roles = array();
  protected $role_config = array(
    'ctlt' => array(),
    'ctlf' => array(),
    'cflt' => array(),
    'cflf' => array(),
  );

  protected $vocabs = array();

  public static function getInfo() {
    return array(
      'name' => 'Term grants',
      'description' => 'Test node access for View tag (create) and Add tag (list) grants.',
      'group' => 'Taxonomy Access Control',
    );
  }

  public function setUp() {
    parent::setUp();

    // Configure roles with no additional permissions.
    foreach ($this->role_config as $role_name => $permissions) {
      $this->roles[$role_name] = $this->drupalCreateRole(array(), $role_name);
    }

    // Set up our testing taxonomy.

    // We will create four vocabularies:
    // vctlt   Receives both "Create" and "List" in the vocabulary default.
    // vctlf   Receives "Create" but not "List" in the vocabulary default.
    // vcflt   Receives "List" but not "Create" in the vocabulary default.
    // vcflf   Receives neither "Create" nor "List" in the vocabulary default.
    $grant_combos = array(
      'ctlt' => array('create' => TAXONOMY_ACCESS_TERM_ALLOW, 'list' => TAXONOMY_ACCESS_TERM_ALLOW),
      'ctlf' => array('create' => TAXONOMY_ACCESS_TERM_ALLOW, 'list' => TAXONOMY_ACCESS_TERM_DENY),
      'cflt' => array('create' => TAXONOMY_ACCESS_TERM_DENY, 'list' => TAXONOMY_ACCESS_TERM_ALLOW),
      'cflf' => array('create' => TAXONOMY_ACCESS_TERM_DENY, 'list' => TAXONOMY_ACCESS_TERM_DENY),
    );

    // Grant all rows view, update, and delete.
    foreach ($grant_combos as $combo) {
      $combo['view'] = TAXONOMY_ACCESS_NODE_ALLOW;
      $combo['update'] = TAXONOMY_ACCESS_NODE_ALLOW;
      $combo['delete'] = TAXONOMY_ACCESS_NODE_ALLOW;
    }

    // Each vocabulary will have four parent terms in the same fashion:
    // ctlt_parent, ctlf_parent, cflt_parent, and cflf_parent.

    // Each of these_parent terms will have children in each class, as well:
    // ctlt_child, ctlf_child, cflt_child, and cflf_child.

    // So, each vocab looks something like:
    // - ctlt_parent
    // - - ctlt_child
    // - - ctlf_child
    // - - cflt_child
    // - - cflf_child
    // - ctlf_parent
    // - - ctlt_child
    // - - ctlf_child
    // - - cflt_child
    // - - cfl_fchild
    // - cflt_parent
    // - - ctlt_child
    // - - ctlf_child
    // - - cflt_child
    // - - cflf_child
    // - cflf_parent
    // - - ctlt_child
    // - - ctlf_child
    // - - cflt_child
    // - - cflf_child

    // Configure terms, vocabularies, and grants.
    foreach ($grant_combos as $vocab_name => $default_grants) {
      // Create the vocabulary.
      $vocab_name = "v" . $vocab_name;
      $this->vocabs[$vocab_name] = array();
      $this->vocabs[$vocab_name]['vocab'] = parent::createVocab($vocab_name);
      $this->vocabs[$vocab_name]['terms'] = array();
      $vocab = $this->vocabs[$vocab_name]['vocab'];

      // Add a field for the vocabulary to pages.
      $this->createField($vocab_name);

      // Configure default grants for the vocabulary for each role.
      if (!empty($default_grants)) {
        foreach ($this->roles as $name => $role) {
          $default_rows[] =  _taxonomy_access_format_grant_record($vocab->vid, $role, $default_grants, TRUE);
          $this->setUpAssertions[] = array(
            'create' => $default_grants['create'],
            'list' => $default_grants['list'],
            'query' => 'SELECT grant_create, grant_list FROM {taxonomy_access_default} WHERE vid = :vid AND rid = :rid',
            'args' => array(':vid' => $vocab->vid, ':rid' => $role),
            'message' => t('Configured default grants for vocab %vocab, role %role', array('%vocab' => $vocab->machine_name, '%role' => $name)),
          );
        }
      }

      // Create terms.
      foreach ($grant_combos as $parent_name => $parent_grants) {

        // Create parent term.
        $parent_name = $vocab_name . "__" . $parent_name . "_parent";
        $this->vocabs[$vocab_name]['terms'][$parent_name] =
          parent::createTerm($parent_name, $vocab);
        $parent_id = $this->vocabs[$vocab_name]['terms'][$parent_name]->tid;

        // Configure grants for the parent term for each role.
        if (!empty($parent_grants)) {
          foreach ($this->roles as $name => $role) {
            $term_rows[] =  _taxonomy_access_format_grant_record($parent_id, $role, $parent_grants);
            $this->setUpAssertions[] = array(
              'create' => $parent_grants['create'],
              'list' => $parent_grants['list'],
              'query' => 'SELECT grant_create, grant_list FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
              'args' => array(':tid' => $parent_id, ':rid' => $role),
              'message' => t('Configured grants for term %term, role %role', array('%term' => $parent_name, '%role' => $name)),
            );
          }
        }

        // Create child terms.
        foreach ($grant_combos as $child_name => $child_grants) {
          $child_name = $parent_name . "__" . $child_name . "_child";
          $this->vocabs[$vocab_name]['terms'][$child_name] =
            parent::createTerm($child_name, $vocab, $parent_id);
          $child_id = $this->vocabs[$vocab_name]['terms'][$child_name]->tid;

          // Configure grants for the child term for each role.
          if (!empty($child_grants)) {
            foreach ($this->roles as $name => $role) {
              $term_rows[] =  _taxonomy_access_format_grant_record($child_id, $role, $child_grants);
              $this->setUpAssertions[] = array(
                'create' => $child_grants['create'],
                'list' => $child_grants['list'],
                'query' => 'SELECT grant_create, grant_list FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
                'args' => array(':tid' => $child_id, ':rid' => $role),
                'message' => t('Configured grants for term %term, role %role', array('%term' => $child_name, '%role' => $name)),
              );
            }
          }
        }
      }
    }

    // Set the grants.
    taxonomy_access_set_default_grants($default_rows);
    taxonomy_access_set_term_grants($term_rows);
  }

  /**
   * Verifies that all grants were properly stored during setup.
   */
  public function testSetUpCheck() {
    // Check that all records were properly stored.
    foreach ($this->setUpAssertions as $assertion) {
      $r = db_query($assertion['query'], $assertion['args'])->fetchAssoc();
      $this->assertTrue(
        (is_array($r)
          && $r['grant_create'] == $assertion['create']
          && $r['grant_list'] == $assertion['list']),
        $assertion['message']
      );
    }
  }
}

class TaxonomyAccessWeightTest extends DrupalWebTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Weight',
      'description' => 'Test module weight.',
      'group' => 'Taxonomy Access Control',
    );
  }

  public function setUp() {
    parent::setUp('taxonomy_access');
  }

  /**
  * Verifies that this module is weighted below the Taxonomy module.
  */
  public function testWeight() {

    // Verify weight.
    $tax_weight =
      db_query(
        "SELECT weight FROM {system}
         WHERE name = 'taxonomy'")
      ->fetchField();
    $tax_access_weight =
      db_query(
        "SELECT weight FROM {system}
         WHERE name = 'taxonomy_access'")
      ->fetchField();
    $this->assertTrue(
      $tax_access_weight > $tax_weight,
      t("Weight of this module is @tax_access_weight. Weight of the Taxonomy module is @tax_weight.",
      array('@tax_access_weight' => $tax_access_weight, '@tax_weight' => $tax_weight))
    );

    // Disable module and set weight of the Taxonomy module to a high number.
    module_disable(array('taxonomy_access'), TRUE);
    db_update('system')
    ->fields(array('weight' => rand(5000, 9000)))
    ->condition('name', 'taxonomy')
    ->execute();

    // Re-enable module and re-verify weight.
    module_enable(array('taxonomy_access'), TRUE);
    $tax_weight =
      db_query(
        "SELECT weight FROM {system}
         WHERE name = 'taxonomy'")
      ->fetchField();
    $tax_access_weight =
      db_query(
        "SELECT weight FROM {system}
         WHERE name = 'taxonomy_access'")
      ->fetchField();
    $this->assertTrue(
      $tax_access_weight > $tax_weight,
      t("Weight of this module is @tax_access_weight. Weight of the Taxonomy module is @tax_weight.",
      array('@tax_access_weight' => $tax_access_weight, '@tax_weight' => $tax_weight))
    );
  }
}


/**
 * Tests that callbacks are cleaned up when the module is disabled.
 */
class TaxonomyAccessCallbackCleanupTest extends DrupalWebTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Callback Cleanup',
      'description' => 'Test callback cleanup during disabling of module works.',
      'group' => 'Taxonomy Access Control',
    );
  }

  public function setUp() {
    parent::setUp('taxonomy_access');
  }

  /**
   * Verifies that the module's callbacks are cleaned up during disable.
   */
  public function testCallbackCleanup() {

    // The problem only happens on new fields after the module is installed.
    $content_type = $this->drupalCreateContentType();

    // Create a new field with type taxonomy_term_reference.
    $field_name = drupal_strtolower($this->randomName() . '_field_name');
    $field_type = array(
      'field_name' => $field_name,
      'type' => 'taxonomy_term_reference',
      'cardinality' => 1,
    );
    $field_type = field_create_field($field_type);

    // Add an instance of the field to content type.
    $field_instance = array(
      'field_name' => $field_name,
      'entity_type' => 'node',
      'bundle' => $content_type->name
    );
    $field_instance = field_create_instance($field_instance);

    // Trigger hook_disable to see if the callbacks are cleaned up.
    module_disable(array('taxonomy_access'), TRUE);

    // Create a user so that we can check if we can access the node add pages.
    $this->privileged_user = $this->drupalCreateUser(array('bypass node access'));
    $this->drupalLogin($this->privileged_user);

    // If the callbacks are not cleaned up we would get a fatal error.
    $this->drupalGet('node/add/' . $content_type->name);
    $this->assertText(t('Create @name', array('@name' => $content_type->name)), t('New content can be added'));
  }
}
