You are here

restws.test in RESTful Web Services 7

Same filename and directory in other branches
  1. 7.2 restws.test

RESTful web services tests.

File

restws.test
View source
<?php

/**
 * @file
 * RESTful web services tests.
 */
class RestWSTestCase extends DrupalWebTestCase {
  static function getInfo() {
    return array(
      'name' => 'RESTful web services tests',
      'description' => 'Tests CRUD operations via the REST web service.',
      'group' => 'Services',
    );
  }
  function setUp() {
    parent::setUp('restws');
  }

  /**
   * CRUD tests for nodes.
   */
  function testCRUD() {

    // Test Read.
    $title = $this
      ->randomName(8);
    $node = $this
      ->drupalCreateNode(array(
      'title' => $title,
    ));
    $account = $this
      ->drupalCreateUser(array(
      'access resource node',
    ));
    $this
      ->drupalLogin($account);
    $result = $this
      ->httpRequest('node/' . $node->nid . '.json', 'GET', $account);
    $node_array = drupal_json_decode($result);
    $this
      ->assertEqual($node->title, $node_array['title'], 'Node title was received correctly.');
    $this
      ->assertResponse('200', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/json', 'HTTP content type is correct.');

    // Test Create.
    $account = $this
      ->drupalCreateUser(array(
      'access content',
      'bypass node access',
      'access resource node',
    ));
    $title = $this
      ->randomName(8);
    $new_node = array(
      'body' => array(
        LANGUAGE_NONE => array(
          array(),
        ),
      ),
      'title' => $title,
      'type' => 'page',
      'author' => $account->uid,
    );
    $json = drupal_json_encode($new_node);
    $result = $this
      ->httpRequest('node', 'PUT', $account, $json);
    $result_array = drupal_json_decode($result);
    $nid = $result_array['id'];
    $node = node_load($nid);
    $this
      ->assertEqual($title, $node->title, 'Node title in DB is equal to the new title.');
    $this
      ->assertResponse('201', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/json', 'HTTP content type is correct.');

    // Test Update.
    $new_title = $this
      ->randomName(8);
    $json = drupal_json_encode(array(
      'title' => $new_title,
    ));
    $this
      ->httpRequest('node/' . $node->nid, 'POST', $account, $json);

    // Clear the static cache, otherwise we won't see the update.
    $node = node_load($node->nid, NULL, TRUE);
    $this
      ->assertEqual($new_title, $node->title, 'Node title in DB is equal to the updated title.');
    $this
      ->assertResponse('200', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/json', 'HTTP content type is correct.');

    // Test delete.
    $this
      ->httpRequest('node/' . $node->nid, 'DELETE', $account);

    // Clear the static cache, otherwise we won't see the update.
    $node = node_load($node->nid, NULL, TRUE);
    $this
      ->assertFalse($node, 'Node is not in the DB anymore.');
    $this
      ->assertResponse('200', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/json', 'HTTP content type is correct.');
  }

  /**
   * Tests bad requests.
   */
  function testBadRequests() {

    // Assure that nodes without types won't be created.
    $account = $this
      ->drupalCreateUser(array(
      'access content',
      'bypass node access',
      'access resource node',
      'administer users',
    ));
    $title = $this
      ->randomName(8);
    $new_node = array(
      'body' => array(
        LANGUAGE_NONE => array(
          array(),
        ),
      ),
      'title' => $title,
    );
    $json = drupal_json_encode($new_node);
    $result = $this
      ->httpRequest('node', 'PUT', $account, $json);
    $node = entity_load('node', FALSE, array(
      'title' => $title,
    ));
    $this
      ->assertEqual(count($node), 0, "Node wasn't created");
    $this
      ->assertResponse('406', 'Missing bundle: type');
  }

  /**
   * Tests access to restricted input formats.
   */
  public function testBadInputFormat() {
    module_enable(array(
      'php',
    ));

    // Reset the cache of valid permissions so that the PHP code format
    // permission exists.
    $this
      ->checkPermissions(array(), TRUE);

    // Assure that users can't create nodes with unauthorized input formats.
    $unprivileged_account = $this
      ->drupalCreateUser(array(
      'bypass node access',
      'access resource node',
    ));
    $title = $this
      ->randomName(8);
    $new_node = array(
      'body' => array(
        'value' => $this
          ->randomName(30),
        'format' => 'php_code',
      ),
      'title' => $title,
      'type' => 'page',
    );
    $json = drupal_json_encode($new_node);
    $result = $this
      ->httpRequest('node', 'PUT', $unprivileged_account, $json);
    $this
      ->assertResponse('403');
    $this
      ->assertEqual($result, '403 Forbidden: Not authorized to set property body');
    $node = entity_load('node', FALSE, array(
      'title' => $title,
    ));
    $this
      ->assertEqual(count($node), 0, "Node with unauthorized input format wasn't created");

    // Check that the format is allowed if the permission is present.
    $privileged_account = $this
      ->drupalCreateUser(array(
      'bypass node access',
      'access resource node',
      'use text format php_code',
    ));
    $this
      ->httpRequest('node', 'PUT', $privileged_account, $json);
    $this
      ->assertResponse('201');
    $node = entity_load('node', FALSE, array(
      'title' => $title,
    ));
    $this
      ->assertEqual(count($node), 1, "Node was created");
    $node = reset($node);
    $this
      ->assertEqual($node->body[LANGUAGE_NONE][0]['value'], $new_node['body']['value'], 'The new node body has the correct value');
    $this
      ->assertEqual($node->body[LANGUAGE_NONE][0]['format'], 'php_code', 'The new node has the correct format');

    // Check that users can't update nodes with unauthorized input formats.
    $node->body[LANGUAGE_NONE][0]['format'] = 'filtered_html';
    node_save($node);
    $new_body = $this
      ->randomName(30);
    $update = array(
      'body' => array(
        'value' => $new_body,
        'format' => 'php_code',
      ),
    );
    $json = drupal_json_encode($update);
    $result = $this
      ->httpRequest('node/1', 'POST', $unprivileged_account, $json);
    $this
      ->assertResponse('403');
    $this
      ->assertEqual($result, '403 Forbidden: Not authorized to set property body');
    $node = node_load(1, NULL, TRUE);
    $this
      ->assertNotEqual($node->body[LANGUAGE_NONE][0]['value'], $new_body);
    $this
      ->assertEqual($node->body[LANGUAGE_NONE][0]['format'], 'filtered_html');

    // Check that the format is allowed if the permission is present.
    $this
      ->httpRequest('node/1', 'POST', $privileged_account, $json);
    $this
      ->assertResponse('200');
    $node = node_load(1, NULL, TRUE);
    $this
      ->assertEqual($node->body[LANGUAGE_NONE][0]['value'], $new_body);
    $this
      ->assertEqual($node->body[LANGUAGE_NONE][0]['format'], 'php_code');
  }

  /**
   * Test field level access restrictions.
   *
   * @see restws_test_field_access()
   */
  public function testFieldAccess() {
    module_enable(array(
      'restws_test',
    ));

    // Add text field to nodes.
    $field_info = array(
      'field_name' => 'field_text',
      'type' => 'text',
      'entity_types' => array(
        'node',
      ),
    );
    field_create_field($field_info);
    $instance = array(
      'label' => 'Text Field',
      'field_name' => 'field_text',
      'entity_type' => 'node',
      'bundle' => 'page',
      'settings' => array(),
      'required' => FALSE,
    );
    field_create_instance($instance);

    // A user without the "administer users" permission should not be able to
    // create a node with the access protected field.
    $unprivileged_account = $this
      ->drupalCreateUser(array(
      'bypass node access',
      'access resource node',
    ));
    $title = $this
      ->randomName(8);
    $new_node = array(
      'title' => $title,
      'type' => 'page',
      'field_text' => 'test',
    );
    $json = drupal_json_encode($new_node);
    $this
      ->httpRequest('node', 'PUT', $unprivileged_account, $json);
    $this
      ->assertResponse('403');
    $nodes = entity_load('node', FALSE, array(
      'title' => $title,
    ));
    $this
      ->assertEqual(count($nodes), 0, "Node with access protected field wasn't created");

    // Test again with the additional permission, this should work now.
    $privileged_account = $this
      ->drupalCreateUser(array(
      'bypass node access',
      'access resource node',
      'administer users',
    ));
    $this
      ->httpRequest('node', 'PUT', $privileged_account, $json);
    $this
      ->assertResponse('201');
    $node = node_load(1, NULL, TRUE);
    $this
      ->assertEqual($node->field_text[LANGUAGE_NONE][0]['value'], 'test');

    // Update test: unpriviledged users should not be able to change the
    // protected field.
    $update = array(
      'field_text' => 'newvalue',
    );
    $json = drupal_json_encode($update);
    $result = $this
      ->httpRequest('node/1', 'POST', $unprivileged_account, $json);
    $this
      ->assertResponse('403');
    $this
      ->assertEqual($result, '403 Forbidden: Not authorized to set property field_text');
    $node = node_load(1, NULL, TRUE);
    $this
      ->assertEqual($node->field_text[LANGUAGE_NONE][0]['value'], 'test');

    // Check that the update is allowed if the permission is present.
    $this
      ->httpRequest('node/1', 'POST', $privileged_account, $json);
    $this
      ->assertResponse('200');
    $node = node_load(1, NULL, TRUE);
    $this
      ->assertEqual($node->field_text[LANGUAGE_NONE][0]['value'], 'newvalue');
  }

  /**
   * Tests using the xml formatter.
   */
  function testXmlFormatter() {

    // Test Read.
    $account = $this
      ->drupalCreateUser(array(
      'access content',
      'bypass node access',
      'access resource node',
    ));
    $this
      ->drupalLogin($account);
    $title = $this
      ->randomName(8);
    $node = $this
      ->drupalCreateNode(array(
      'title' => $title,
    ));
    $result = $this
      ->drupalGet("node/{$node->nid}", array(), array(
      'Accept: application/xml',
    ));
    $this
      ->assertRaw("<title>{$title}</title>", 'XML has been generated.');

    // Test update.
    $new_title = 'foo';
    $result = $this
      ->httpRequest('node/' . $node->nid, 'POST', $account, "<node><title>{$new_title}</title></node>", 'xml');

    // Clear the static cache, otherwise we won't see the update.
    $node = node_load($node->nid, NULL, TRUE);
    $this
      ->assertEqual($new_title, $node->title, 'Node title in DB is equal to the updated title.');
    $this
      ->assertResponse('200', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/xml', 'HTTP content type is correct.');
  }

  /**
   * Test requests to non-existing resources and other errors.
   */
  function testErrors() {

    // Read non-existing resource.
    $random_nid = rand(1, 1000);
    $result = $this
      ->httpRequest('node/' . $random_nid, 'GET');
    $this
      ->assertResponse('404', 'HTTP response code is correct.');

    // Update a node with an unknown property.
    $account = $this
      ->drupalCreateUser(array(
      'access content',
      'bypass node access',
      'access resource node',
    ));
    $node = $this
      ->drupalCreateNode();
    $property_name = $this
      ->randomName(8);
    $json = drupal_json_encode(array(
      $property_name => $property_name,
    ));
    $result = $this
      ->httpRequest('node/' . $node->nid, 'POST', $account, $json);
    $this
      ->assertEqual($result, "406 Not Acceptable: Unknown data property {$property_name}.", 'Response body is correct');
    $this
      ->assertResponse('406', 'HTTP response code is correct.');

    // Create a node with an unknown property.
    $title = $this
      ->randomName(8);
    $new_node = array(
      'body' => array(
        LANGUAGE_NONE => array(
          array(),
        ),
      ),
      'title' => $this
        ->randomName(8),
      'type' => 'page',
      'author' => $account->uid,
      $property_name => $property_name,
    );
    $json = drupal_json_encode($new_node);
    $result = $this
      ->httpRequest('node', 'PUT', $account, $json);
    $this
      ->assertEqual($result, "406 Not Acceptable: Unknown data property {$property_name}.", 'Response body is correct');
    $this
      ->assertResponse('406', 'HTTP response code is correct.');

    // Simulate a CSRF attack without the required token.
    $new_title = 'HACKED!';
    $json = drupal_json_encode(array(
      'title' => $new_title,
    ));
    $this
      ->curlExec(array(
      CURLOPT_HTTPGET => FALSE,
      CURLOPT_POST => TRUE,
      CURLOPT_CUSTOMREQUEST => 'POST',
      CURLOPT_POSTFIELDS => $json,
      CURLOPT_URL => url('node/' . $node->nid, array(
        'absolute' => TRUE,
      )),
      CURLOPT_NOBODY => FALSE,
      CURLOPT_HTTPHEADER => array(
        'Content-Type: application/json',
      ),
    ));
    $this
      ->assertResponse(403);

    // Clear the static cache, otherwise we won't see the update.
    $node = node_load($node->nid, NULL, TRUE);
    $this
      ->assertNotEqual($node->title, $new_title, 'Node title was not updated in the database.');

    // Simulate a cache poisoning attack where JSON could get into the page
    // cache.
    // Grant node resource access to anonymous users.
    user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array(
      'access resource node',
    ));

    // Enable page caching.
    variable_set('cache', 1);

    // Reset cURL here to delete any stored request settings.
    unset($this->curlHandle);

    // Request the JSON representation of the node.
    $this
      ->drupalGet("node/{$node->nid}", array(), array(
      'Accept: application/json',
    ));
    $this
      ->assertUrl("node/{$node->nid}.json", array(), 'Requesting a resource with JSON Accept header redirects to the .json URL.');

    // Now request the HTML representation.
    $result = $this
      ->drupalGet("node/{$node->nid}");
    $content_type = $this
      ->drupalGetHeader('content-type');
    $this
      ->assertNotEqual($content_type, 'application/json', 'Content type header is not JSON after requesting HTML.');
    $this
      ->assertNull(drupal_json_decode($result), 'Response body is not JSON after requesting HTML.');
  }

  /**
   * Test that sensitive user data is hidden for the "access user profiles"
   * permission.
   */
  function testUserPermissions() {

    // Test other user with "acces user profiles" permission.
    $test_user = $this
      ->drupalCreateUser();
    $account = $this
      ->drupalCreateUser(array(
      'access resource user',
      'access user profiles',
    ));
    $result = $this
      ->httpRequest('user/' . $test_user->uid . '.json', 'GET', $account);
    $user_array = drupal_json_decode($result);
    $this
      ->assertEqual($test_user->name, $user_array['name'], 'User name was received correctly.');
    $this
      ->assertFalse(isset($user_array['mail']), 'User mail is not present in the response.');
    $this
      ->assertFalse(isset($user_array['roles']), 'User roles are not present in the response.');
    $this
      ->assertResponse('200', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/json', 'HTTP content type is correct.');

    // Test the own user - access to sensitive information should be allowed.
    $result = $this
      ->httpRequest('user/' . $account->uid . '.json', 'GET', $account);
    $user_array = drupal_json_decode($result);
    $this
      ->assertEqual($account->name, $user_array['name'], 'User name was received correctly.');
    $this
      ->assertEqual($account->mail, $user_array['mail'], 'User mail is present in the response.');
    $role_keys = array_keys($account->roles);
    $this
      ->assertEqual(sort($role_keys), sort($user_array['roles']), 'User roles are present in the response.');
    $this
      ->assertResponse('200', 'HTTP response code is correct.');
    $this
      ->assertEqual(curl_getinfo($this->curlHandle, CURLINFO_CONTENT_TYPE), 'application/json', 'HTTP content type is correct.');
  }

  /**
   * Helper function to issue a HTTP request with simpletest's cURL.
   */
  protected function httpRequest($url, $method, $account = NULL, $body = NULL, $format = 'json') {
    if (isset($account)) {
      unset($this->curlHandle);
      $this
        ->drupalLogin($account);
    }
    if (in_array($method, array(
      'POST',
      'PUT',
      'DELETE',
    ))) {

      // GET the CSRF token first for writing requests.
      $token = $this
        ->drupalGet('restws/session/token');
    }
    switch ($method) {
      case 'GET':
        return $this
          ->curlExec(array(
          CURLOPT_HTTPGET => TRUE,
          CURLOPT_URL => url($url, array(
            'absolute' => TRUE,
          )),
          CURLOPT_NOBODY => FALSE,
        ));
      case 'PUT':
        return $this
          ->curlExec(array(
          CURLOPT_HTTPGET => FALSE,
          CURLOPT_CUSTOMREQUEST => 'PUT',
          CURLOPT_POSTFIELDS => $body,
          CURLOPT_URL => url($url, array(
            'absolute' => TRUE,
          )),
          CURLOPT_NOBODY => FALSE,
          CURLOPT_HTTPHEADER => array(
            'Content-Type: application/' . $format,
            'X-CSRF-Token: ' . $token,
          ),
        ));
      case 'POST':
        return $this
          ->curlExec(array(
          CURLOPT_HTTPGET => FALSE,
          CURLOPT_POST => TRUE,
          CURLOPT_POSTFIELDS => $body,
          CURLOPT_URL => url($url, array(
            'absolute' => TRUE,
          )),
          CURLOPT_NOBODY => FALSE,
          CURLOPT_HTTPHEADER => array(
            'Content-Type: application/' . $format,
            'X-CSRF-Token: ' . $token,
          ),
        ));
      case 'DELETE':
        return $this
          ->curlExec(array(
          CURLOPT_HTTPGET => FALSE,
          CURLOPT_CUSTOMREQUEST => 'DELETE',
          CURLOPT_URL => url($url, array(
            'absolute' => TRUE,
          )),
          CURLOPT_NOBODY => FALSE,
          CURLOPT_HTTPHEADER => array(
            'X-CSRF-Token: ' . $token,
          ),
        ));
    }
  }

}

Classes

Namesort descending Description
RestWSTestCase @file RESTful web services tests.