diff --git tests/rest/ServicesRESTTestCase.php tests/rest/ServicesRESTTestCase.php
new file mode 100644
index 0000000..b70424e
--- /dev/null
+++ tests/rest/ServicesRESTTestCase.php
@@ -0,0 +1,176 @@
+<?php
+// $Id$
+
+/**
+ * Helper class to provide call functions (GET, POST, PUT, DELETE).
+ */
+class ServicesRESTTestCase extends DrupalWebTestCase {
+
+  /**
+   * Perform GET request.
+   *
+   * @param type $path
+   * @param type $args
+   * @param type $options
+   * @param type $headers
+   */
+  function servicesGET($path, $args = array(), $options = array(), $headers = array()) {
+    $options['absolute'] = TRUE;
+
+    // We re-using a CURL connection here. If that connection still has certain
+    // options set, it might change the GET into a POST. Make sure we clear out
+    // previous options.
+    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
+    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
+
+    // Replace original page output with new output from redirected page(s).
+    if (($new = $this->checkForMetaRefresh())) {
+      $out = $new;
+    }
+    $this->verbose('Services GET request to: ' . $path .
+                   '<hr />Arguments: ' . highlight_string('<?php ' . var_export($args, TRUE), TRUE) .
+                   '<hr />' . $out);
+    return $out;
+  }
+
+  /**
+   * Perform POST request.
+   *
+   * @param <type> $path
+   * @param <type> $args
+   * @param <type> $headers
+   * @return <type>
+   */
+  function servicesPOST($path, $args, $options = array(), $headers = array()) {
+    $options['absolute'] = TRUE;
+
+    $post = array();
+    foreach ($args as $key => $value) {
+      // Encode according to application/x-www-form-urlencoded
+      // Both names and values needs to be urlencoded, according to
+      // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
+      $post[$key] = urlencode($key) . '=' . urlencode($value);
+    }
+    $post = implode('&', $post);
+
+    $out = $this->curlExec(array(CURLOPT_URL => url($path, $options), CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $post, CURLOPT_HTTPHEADER => $headers));
+
+    $this->verbose('Services POST request to: ' . $path .
+                     '<hr />Arguments: ' . highlight_string('<?php ' . var_export($args, TRUE), TRUE) .
+                     '<hr />' . $out);
+    return $out;
+  }
+
+  /**
+   * Perform DELETE request.
+   *
+   * @param <type> $path
+   * @param <type> $args
+   * @param <type> $headers
+   * @return <type>
+   */
+  function servicesDELETE($path, $args = array(), $options = array(), $headers = array()) {
+    $options['absolute'] = TRUE;
+    $options['query'] = $args;
+
+    $out = $this->curlExec(array(
+              CURLOPT_URL => url($path, $options),
+              CURLOPT_CUSTOMREQUEST => 'DELETE',
+            ));
+
+    $this->verbose('Services DELETE request to: ' . $path .
+                     '<hr />Arguments: ' . highlight_string('<?php ' . var_export($args, TRUE), TRUE) .
+                     '<hr />' . $out);
+    return $out;
+  }
+
+  /**
+   * Perform PUT request.
+   *
+   * @param <type> $path
+   * @param <type> $args
+   * @param <type> $headers
+   * @return <type>
+   */
+  function servicesPUT($path, $args, $options = array(), $headers = array()) {
+    $options['absolute'] = TRUE;
+
+    $serialize_args = serialize($args);
+
+    // Set up headers so arguments will parsed properly.
+    $headers = array("Content-type: application/vnd.php.serialized; charset=iso-8859-1");
+
+    $fh = fopen('php://memory', 'rw+');
+    fwrite($fh, $serialize_args);
+    fseek($fh, 0);
+
+    $out = $this->curlExec(array(
+              CURLOPT_URL => url($path, $options),
+              CURLOPT_PUT => TRUE,
+              CURLOPT_INFILE => $fh,
+              CURLOPT_INFILESIZE => strlen($serialize_args),
+              CURLOPT_HTTPHEADER => $headers,
+            ));
+
+    $this->verbose('Services PUT request to: ' . $path .
+                     '<hr />Arguments: ' . highlight_string('<?php ' . var_export($args, TRUE), TRUE) .
+                     '<hr />' . $out);
+    return $out;
+  }
+
+  /**
+   * Performs a cURL exec with the specified options after calling curlConnect().
+   *
+   * @param $curl_options
+   *   Custom cURL options.
+   * @return
+   *   Content returned from the exec.
+   */
+  protected function curlExec($curl_options) {
+    $this->curlInitialize();
+    $url = empty($curl_options[CURLOPT_URL]) ? curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL) : $curl_options[CURLOPT_URL];
+    if (!empty($curl_options[CURLOPT_POST])) {
+      // This is a fix for the Curl library to prevent Expect: 100-continue
+      // headers in POST requests, that may cause unexpected HTTP response
+      // codes from some webservers (like lighttpd that returns a 417 error
+      // code). It is done by setting an empty "Expect" header field that is
+      // not overwritten by Curl.
+      $curl_options[CURLOPT_HTTPHEADER][] = 'Expect:';
+    }
+    curl_setopt_array($this->curlHandle, $this->additionalCurlOptions + $curl_options);
+
+    // Reset headers and the session ID.
+    $this->session_id = NULL;
+    $this->headers = array();
+
+    $this->drupalSetContent(curl_exec($this->curlHandle), curl_getinfo($this->curlHandle, CURLINFO_EFFECTIVE_URL));
+
+    // Analyze the method for log message.
+    $method = '';
+    if (!empty($curl_options[CURLOPT_NOBODY])) {
+      $method = 'HEAD';
+    }
+
+    if (empty($method) && !empty($curl_options[CURLOPT_PUT])) {
+      $method = 'PUT';
+    }
+
+    if (empty($method) && !empty($curl_options[CURLOPT_CUSTOMREQUEST])) {
+      $method = strtoupper($curl_options[CURLOPT_CUSTOMREQUEST]);
+    }
+
+    if (empty($method)) {
+      $method = empty($curl_options[CURLOPT_POSTFIELDS]) ? 'GET' : 'POST';
+    }
+
+    $message_vars = array(
+      '!method' => $method,
+      '@url' => $url,
+      '@status' => curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE),
+      '!length' => format_size(strlen($this->content))
+    );
+    $message = t('!method @url returned @status (!length).', $message_vars);
+    $this->assertTrue($this->content !== FALSE, $message, t('Browser'));
+    return $this->drupalGetContent();
+  }
+}
diff --git tests/rest/node_resource/ServicesRESTTestCaseNodeResource.test tests/rest/node_resource/ServicesRESTTestCaseNodeResource.test
new file mode 100644
index 0000000..5f027b0
--- /dev/null
+++ tests/rest/node_resource/ServicesRESTTestCaseNodeResource.test
@@ -0,0 +1,177 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Test for node resource REST server.
+ */
+
+// Load ServicesRESTTestCase class.
+module_load_include('php', 'services', 'tests/rest/ServicesRESTTestCase');
+
+/**
+ * Tests for Node Resource.
+ */
+class ServicesRESTTestCaseNodeResource extends ServicesRESTTestCase {
+  protected $user;
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Unit tests for node resource',
+      'description' => 'Tests CRUD of node resource with REST calls.',
+      'group' => 'Services REST',
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  public function setUp() {
+    parent::setUp('ctools', 'autoload', 'inputstream', 'services', 'rest_server');
+
+    // Create user and log in.
+    $this->user = $this->drupalCreateUser(array('administer services', 'administer nodes'));
+    $this->drupalLogin($this->user);
+
+    // Create test endpoint.
+    $edit = array(
+      'name' => 'test_endpoint',
+      'title' => 'Test endpoint title',
+      'server' => 'rest_server',
+      'path' => 'endpoint',
+    );
+    $this->drupalPost('admin/build/services/add', $edit, t('Save and proceed'));
+
+    // Enable node resouce.
+    $edit = array(
+      'resources[node][operations][create][enabled]' => 1,
+      'resources[node][operations][retrieve][enabled]' => 1,
+      'resources[node][operations][update][enabled]' => 1,
+      'resources[node][operations][delete][enabled]' => 1,
+      'resources[node][operations][index][enabled]' => 1,
+    );
+    $this->drupalPost('admin/build/services/test_endpoint/resources', $edit, t('Save'));
+  }
+
+  /**
+   * Test node create.
+   */
+  function testNodeCreate() {
+    // Node values.
+    $node = array(
+      'body'      => $this->randomName(32),
+      'title'     => $this->randomName(8),
+      'comment'   => 2,
+      'format'    => FILTER_FORMAT_DEFAULT,
+      'moderate'  => 0,
+      'promote'   => 0,
+      'revision'  => 1,
+      'status'    => 1,
+      'sticky'    => 0,
+      'type'      => 'page',
+    );
+
+    $path = 'endpoint/node.php';
+    $response = $this->servicesPOST($path, $node);
+    // Unserialize the response.
+    $response = unserialize($response);
+
+    $nid = $response['nid'];
+    $node_internal = node_load($nid);
+
+    // Compare created node.
+    $node_diff = array_diff((array)$node, (array)$node_internal);
+    $this->assertTrue(empty($node_diff), t('Node has been created properly.'));
+  }
+
+  /**
+   * Test for node retrieve.
+   */
+  function testNodeRetrieve() {
+    // Create node.
+    $node = $this->drupalCreateNode();
+    // Retrieve this node via REST call.
+    $node_retrieve_serialized = $this->servicesGET('endpoint/node/' . $node->nid . '.php');
+    // Unserialize retrieved node.
+    $node_retrieve = unserialize($node_retrieve_serialized);
+    // Compare nodes.
+    $node_diff = array_diff((array)$node, (array)$node_retrieve);
+
+    $this->assertTrue(empty($node_diff), t('Retrieved node is the same as created.'));
+  }
+
+  /**
+   * Test for update method.
+   */
+  function testNodeUpdate() {
+    // Create the node
+    $node = $this->drupalCreateNode();
+    // Change the title and body of the node.
+    $title = $this->randomName();
+    $body = $this->randomString();
+
+    // Do PUT call.
+    $args = array(
+      'title' => $title,
+      'body'  => $body,
+      'type'  => $node->type,
+    );
+    $path = 'endpoint/node/' . $node->nid . '.php';
+    $response = $this->servicesPUT($path, $args);
+
+    // Load node by new title.
+    $node_updated = $this->drupalGetNodeByTitle($title);
+
+    $node_load = node_load($node->nid, NULL, TRUE);
+
+    // Assert that it is the same node.
+    $this->assertTrue(($node->nid == $node_updated->nid && $node_updated->body == $body), t('Node has been updated properly.'));
+  }
+
+  /**
+   * Test for node delete.
+   */
+  function testNodeDelete() {
+    // Create the node
+    $node = $this->drupalCreateNode();
+
+    // Delete the node.
+    $this->servicesDELETE('endpoint/node/' . $node->nid . '.php');
+
+    // Load node. Explicitly load not from cache.
+    $deleted_node = node_load($node->nid, NULL, TRUE);
+
+    $this->assertFalse($deleted_node, t('Node has been deleted successfully.'));
+  }
+
+  /**
+   * Test for node index.
+   */
+  function testNodeIndex() {
+    // Create 2 nodes.
+    $node1 = $this->drupalCreateNode();
+    // Second node created later than first node.
+    $node2 = $this->drupalCreateNode(array('created' => time() + 10));
+    // Index all available nodes. As current user has 'administer nodes'
+    // permissions he should get back both nodes.
+    $node_index_serialized = $this->servicesGET('endpoint/node.php');
+
+    $node_index = unserialize($node_index_serialized);
+
+    // Nodes are sorted by sticky and created DESC.
+    $remote_node1 = $node_index[1];
+    $remote_node2 = $node_index[0];
+    // Unset uri as this property is not in local nodes.
+    unset($remote_node1->uri);
+    unset($remote_node2->uri);
+    // Compare first node.
+    $node_diff = array_diff((array)$remote_node1, (array)$node1);
+    $this->assertTrue(empty($node_diff), t('First listed node is the same as created.'));
+    // Compare second node.
+    $node_diff = array_diff((array)$remote_node2, (array)$node2);
+    $this->assertTrue(empty($node_diff), t('Second listed node is the same as created.'));
+  }
+}
