diff --git a/class/JsonNode.php b/class/JsonNode.php
new file mode 100644
index 0000000..d41e4b8
--- /dev/null
+++ b/class/JsonNode.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Wrapper around the $json obejct tree
+ */
+
+class JsonNode {
+  /**
+   * @var object The processed data structure by NodeJsonFetcher
+   */
+  private $json;
+
+  /**
+   * @var array processed comments with author attached.
+   */
+  private $commentors;
+
+  /**
+   * @var array processed files with owner attached.
+   */
+  private $files;
+
+  /**
+   * @param object $json
+   */
+  function __construct($json) {
+    $this->json = $json;
+    if (!isset($json->ALL_META_DATA)) {
+      throw new \Exception("NO metadata found.");
+    }
+
+    // Prepare related datasets.
+    // TODO: taxonomy_term
+    $this->setCommentors();
+    $this->setFiles();
+    $this->setMatchedFiles();
+  }
+
+  /**
+   * Magic getter to get into json properties or ALL_META_DATA keys
+   */
+  public function __get($name) {
+    if (strpos($name, '_') === 0) {
+      $name = substr($name, 1);
+      if (array_key_exists($name, $this->json->ALL_META_DATA)) {
+        return $this->json->ALL_META_DATA[$name];
+      }
+    }
+    else {
+      if (array_key_exists($name, $this->json)) {
+        return $this->json->{$name};
+      }
+    }
+
+    $trace = debug_backtrace();
+    trigger_error(
+        'Undefined property via __get(): ' . $name .
+        ' in ' . $trace[0]['file'] .
+        ' on line ' . $trace[0]['line'],
+        E_USER_NOTICE);
+    return null;
+  }
+
+  /**
+   * Helper for getting to the node based properties.
+   *
+   * These are returns by only fetching the node like
+   *
+   */
+  public function getBaseProperty($name) {
+    return $this->json->{$name};
+  }
+
+  // TODO : add more base properties like taxonomy_vocabulary_9
+
+  /**
+   * Get the author object for this node.
+   *
+   * Note this is a two step property.
+   *
+   * @see JsonNode::_get().
+   */
+  public function getAuthor() {
+    $uid = $this->author->id;
+    return $this->_user[$uid];
+  }
+
+  public function getCommentors() {
+    return $this->commentors;
+  }
+
+  private function setCommentors() {
+    $result = array();
+    $comments = $this->getBaseProperty('comments');
+    foreach($comments as $index => $comment) {
+      $value = $this->_comment[$comment->id];
+      // Overwrite author with exploded value;
+      $value->author = $this->_user[$value->author->id];
+      $result[] = $value;
+    }
+    $this->commentors = $result;
+  }
+
+  public function getFiles() {
+    return $this->files;
+  }
+
+  private function setFiles() {
+    $result = array();
+    $files = $this->getBaseProperty('field_issue_files');
+    foreach($files as $fileWrapper) {
+      $file = $fileWrapper->file;
+      $value = $this->_file[$file->id];
+      // Overwrite owner with exploded value;
+      $value->owner = $this->_user[$value->owner->id];
+      $result[] = $this->_file[$file->id];
+    }
+    $this->files = $result;
+  }
+
+  /**
+   * Match files to comments
+   *
+   * TODO: make this work :(
+   */
+  private function setMatchedFiles() {
+    $files = $this->getFiles();
+    if (!count($files)) {
+      // Nothing to do.
+      return;
+    }
+
+    $comments = $this->getCommentors();
+    echo PHP_EOL;
+    echo "Comments : " . count($comments) . PHP_EOL;
+    echo "Files    : " . count($files) . PHP_EOL;
+    $file = array_shift($files);
+
+    // Each file should have a comment so iterator over comments
+    foreach($comments as $comment) {
+      $diff = ($comment->created - $file->timestamp);
+      $cuid = $comment->author->uid;
+      $fuid = $file->owner->uid;
+      echo "C vs F : $diff : $cuid vs $fuid" . PHP_EOL;
+      if (abs($diff) < 1000) {
+        $file->comment = $comment;
+        // Get next
+        $file = array_shift($files);
+        if (empty($file)) {
+          // done
+          break;
+        }
+      }
+    }
+    if (count($files)>0) {
+      echo "ERROR: files left over: " . count($files) . PHP_EOL;
+    }
+  }
+
+}
diff --git a/class/NodeJsonFetcher.php b/class/NodeJsonFetcher.php
new file mode 100644
index 0000000..d3a6d39
--- /dev/null
+++ b/class/NodeJsonFetcher.php
@@ -0,0 +1,168 @@
+<?php
+
+class NodeJsonFetcher {
+
+  // As RestWS is configured to change the resources
+  // through hook_restws_resource_info_alter() we need to add /api-d7
+  /** @var string */
+  private $site = "https://www.drupal.org/api-d7";
+
+  /** @var string */
+  private $cacheDir;
+
+  public function getSite() {
+    return $this->site;
+  }
+
+  public function setSite($site) {
+    $this->site = $site;
+  }
+
+  public function getCacheDir() {
+    return $this->cacheDir;
+  }
+
+  public function setCacheDir($dir) {
+    $this->cacheDir = $dir;
+  }
+
+  /**
+   * Fetches the full REST resources found in the fetched node.
+   *
+   * The fetched resources are cached form obvious performance reason.
+   *
+   * TODO: found out cache invalidation.
+   */
+  public function fetchFullNode($nid) {
+    $node = $this->downloadNode($nid);
+    $store = array();
+    if (!isset($node->author)) {
+      echo "No author for node ... devdrupal quirk!?!" . PHP_EOL;
+      return;
+    }
+    $author = $node->author;
+    $this->fetchResource($store, $author);
+
+    $taxonomies = $node->taxonomy_vocabulary_9;
+    foreach ($taxonomies as $resource) {
+      $this->fetchResource($store, $resource);
+    }
+    $comments = $node->comments;
+    foreach ($comments as $comment) {
+      // We could use $comment->uri if using Accept header and getting response
+      $result = $this->fetchResource($store, $comment);
+      $this->fetchResource($store, $result->author);
+    }
+
+    foreach ($node->field_issue_files as $file) {
+      // $issue_file is a compound object of display + file
+      $result = $this->fetchResource($store, $file->file);
+      $this->fetchResource($store, $result->owner);
+    }
+    $node->ALL_META_DATA = $store;
+    return $node;
+  }
+
+  /**
+   * Fetch and store resource as json.
+   *
+   * @param array &$store
+   *   In memory structure of all already fetched resources.
+   * @param object $resource
+   *   A drupal.org RestWs result.
+   *
+   * @see NodeJsonFetcher::fetchFullNode()
+   */
+  function fetchResource(&$store, $resource) {
+    $uri = $resource->resource . '/' . $resource->id;
+    $result = $this->fetchJson($uri);
+    $store[$resource->resource][$resource->id] = $result;
+    return $result;
+  }
+
+  public function downloadNode($nid) {
+    return $this->fetchJson("node/$nid");
+  }
+
+  function fetchJson($uri) {
+    if (!$content = $this->cache_get($uri)) {
+      $content = $this->fetchUri($uri);
+    }
+    $data = json_decode($content);
+    $this->cache_set($uri, $data);
+    //$this->assertJson($content, "Fetched json from $uri");
+    return $data;
+  }
+
+  function fetchUri($uri) {
+    static $count = 0;
+    $count++;
+
+    $url = $this->buildJsonUri($uri);
+    $options = array(
+      'http' => array(
+        'method' => "GET",
+        'header' => "Accept-language: en\r\nAccept: application/json\r\n" .
+        "Cookie: foo=bar\r\n" . // check function.stream-context-create on php.net
+        "User-Agent: Drush\r\n",
+      ),
+      // Is this the way to allow https invalid certificate?
+      // current status: NO!
+      // http://www.php.net/manual/en/context.ssl.php
+      'ssl' => array(
+        'allow_self_signed' => TRUE,
+        'verify_peer' => FALSE,
+      ),
+    );
+    //drush_download_file($url);
+    drush_log("Fetching ($count): $uri");
+    $context = stream_context_create($options);
+    $file = @file_get_contents($url, false, $context);
+    if (FALSE === $file) {
+      $error = json_encode(error_get_last());
+      //var_dump($error);
+      return $error;
+    }
+    return $file;
+  }
+
+  /**
+   * Generate the full URL to fetch the given $uri.
+   *
+   * Depending on the Rest endpoint this could end with .json
+   *
+   * @param string $uri
+   *   The resource URI given like node/12 or user/12
+   */
+  function buildJsonUri($uri) {
+    return $this->getSite() . '/' . $uri . '.json';
+  }
+
+  /**
+   * Store item into cache
+   *
+   * TODO: use drush cache backend?
+   * TODO: remove __DIR__ path.
+   * @param string $uri
+   *   resource id like 'node/12'
+   * @param object $json
+   *
+   */
+  function cache_set($uri, $json) {
+    $path = $this->getCacheDir() . '/' . $uri . '.json';
+    //drush_log("Cache set: '$uri' - '$path'");
+    file_put_contents($this->getCacheDir() . '/' . $uri . '.json', json_encode($json, JSON_PRETTY_PRINT));
+  }
+
+  function cache_get($uri) {
+    $file = $this->getCacheDir() . '/' . $uri . '.json';
+    @mkdir(dirname($file), 0777, TRUE);
+
+    if (file_exists($file)) {
+      return file_get_contents($file);
+    }
+    drush_log("Cache miss: '$uri' - '$file'");
+    return;
+  }
+
+}
diff --git a/iq.drush.inc b/iq.drush.inc
index 3ae3ff1..cec65ac 100644
--- a/iq.drush.inc
+++ b/iq.drush.inc
@@ -5,7 +5,7 @@
  *  The drush Issue Queue manager
  */
 
-
+require __DIR__ . '/class/NodeJsonFetcher.php';
 
 /**
  * Implementation of hook_drush_command().
@@ -1325,6 +1325,52 @@ function drush_iq_issue_number($issue_spec) {
 }
 
 /**
+ * Produces a Node fetcher.
+ */
+function _drush_iq_get_fetcher() {
+  static $fetcher;
+
+  if (isset($fetcher)) {
+    return $fetcher;
+  }
+
+  $fetcher = new NodeJsonFetcher();
+
+  // "http://drupal:drupal@project-drupal.redesign.devdrupal.org";
+  $fetcher->setSite("http://www.drupal.org/api-d7");
+
+  // TODO: cache this dir;
+  $fetcher->setCacheDir(__DIR__ . '/tests/data/download');
+  return $fetcher;
+}
+
+/**
+ * Fetch all content belonging to a node.
+ */
+function _drush_iq_fetch_full_node($nid) {
+  $fetcher = _drush_iq_get_fetcher();
+  return $fetcher->fetchFullNode($nid);
+}
+
+/**
+ * Fetch the node base json for further usage.
+ *
+ * @see _drush_iq_fetch_full_node().
+ */
+function _drush_iq_fetch_node($nid) {
+  $fetcher = _drush_iq_get_fetcher();
+  return $fetcher->downloadNode($nid);
+}
+
+/**
+ * Fetch the given resource type
+ */
+function _drush_iq_fetch_resource_by_type($id, $type = 'node') {
+  $fetcher = _drush_iq_get_fetcher();
+  return $fetcher->fetchJson("$type/$id");
+}
+
+/**
  *  Given a node id, fetch and decode the issue info json from drupal.org.
  */
 function drush_iq_download_info($issue_info_id) {
diff --git a/iqInternalTest.php b/iqInternalTest.php
new file mode 100644
index 0000000..b1ac38f
--- /dev/null
+++ b/iqInternalTest.php
@@ -0,0 +1,118 @@
+<?php
+
+/*
+ * @file
+ *   PHPUnit Tests for Drush Issue Queue command. This uses Drush's own test
+ *   framework, based on PHPUnit.  To run the tests, use:
+ *
+ *      ./runtests.sh .
+ *
+ *   This is equivalent to:
+ *
+ *     phpunit --bootstrap=/path/to/drush/tests/drush_testcase.inc .
+ *
+ *   Note that we are pointing to the drush_testcase.inc file under /tests
+ *   directory in drush.
+ */
+use \Unish\CommandUnishTestCase;
+
+require __DIR__ . '/iq.drush.inc';
+require_once __DIR__ . '/class/NodeJsonFetcher.php';
+require_once __DIR__ . '/class/JsonNode.php';
+
+if (!function_exists('drush_log')) {
+
+  function drush_log($m) {
+    echo $m . "\n\n";
+  }
+
+  function dt($t) {
+    return $t;
+  }
+
+}
+
+class iqInternalTestCase extends CommandUnishTestCase {
+
+  private $fetcher;
+  private $nodesToFetch = 0;
+
+  public function setup() {
+    $this->nodesToFetch = 7;
+    $this->fetcher = new NodeJsonFetcher();
+
+    // "http://drupal:drupal@project-drupal.redesign.devdrupal.org";
+    $this->fetcher->setSite("http://www.drupal.org/api-d7");
+    $this->fetcher->setCacheDir(__DIR__ . '/tests/data/download');
+  }
+
+  /**
+   * Fetches a set of nodes and their related resources.
+   *
+   * Note the list of issues changes depending on the $this->site used.
+   */
+  public function testDownloadIssueData() {
+    foreach ($this->getNids() as $nid) {
+      $node = $this->fetcher->fetchFullNode($nid);
+
+      $uri = $this->fetcher->getCacheDir() . '/' . "node/$nid.json";
+      $this->assertFileExists($uri, "File exists: $uri");
+      $data = file_get_contents($uri);
+      $this->assertFalse(empty($data), "Data is read");
+      $json = json_decode($data, TRUE);
+    }
+  }
+
+  public function testDrushIq() {
+    $fetcher = _drush_iq_get_fetcher();
+    $this->assertNotNull($fetcher);
+    foreach ($this->getNids() as $nid) {
+      $json = _drush_iq_fetch_node($nid);
+      $this->assertEquals($json->nid, $nid, "NID found");
+      $this->isNode($json);
+
+      $json = _drush_iq_fetch_full_node($nid);
+      $this->assertEquals($json->nid, $nid, "NID found");
+      $this->isFullNode($json);
+
+      // Just the first node
+      //break;
+    }
+  }
+
+  private function isFullNode($json) {
+    $node = new JsonNode($json);
+
+    echo "Author uid: " . $node->getAuthor()->uid . PHP_EOL;
+    echo "Meta keys: " . join(",", array_keys($json->ALL_META_DATA)) . PHP_EOL;
+    echo "Comments:" . PHP_EOL;
+    foreach ($node->getCommentors() as $comment) {
+      echo $comment->author->name . " : $comment->subject " . PHP_EOL;
+    };
+    echo "Files:" . PHP_EOL;
+    foreach ($node->getFiles() as $file) {
+      echo $file->owner->name . " : $file->mime $file->name" . " - matched comment " . (isset($file->comment) ? "ok" : "NO") . PHP_EOL;
+    };
+  }
+
+  private function isNode($json) {
+
+    echo join(", ", array_keys((array) $json)) . PHP_EOL;
+
+    $this->assertNotEmpty($json->body, "BODY found");
+    $this->assertNotEmpty($json->author, "AUTHOR found");
+  }
+
+  /**
+   * Return all or a fraction of configured nids.
+   */
+  private function getNids() {
+    // Not all nodes are on test env
+    // Run jQuery on an issue overview page
+    // list = [];jQuery("a[href^='/node/2']").each(function(){list.push(jQuery(this).attr('href').substring(6));}); list.join(",");
+    $nid_list = "2278541,2259261,2278415,2282599,2281609,2283281,2283293,2274167,2283969,2284025,2246665,2275499,2284017,2281419,2260043,2278715,2281475,2283367,2283851,2278965,2283977,2281659,2281699,2283929,2252713,2280195,2262275,2283877,2281325,2256679,2277981,2281905,2261083,2277281,2247779,2283637,2283301,2256521,2277753,2282161,2283717,2254153,2283711,2283703,2283553,2283675,2282519,2283601,2262247,2249089";
+    $nids = explode(",", $nid_list);
+    return array_slice($nids, 0, min($this->nodesToFetch, count($nids)));
+  }
+
+}
diff --git a/runtests.sh b/runtests.sh
index addb494..30a9481 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -8,7 +8,10 @@
 # Any parameters that may be passed to phpunit may also be used
 # with runtests.sh.
 
-DRUSH_PATH="`which drush`"
+# Follow symlink
+DRUSH_BIN="`which drush`"
+DRUSH_PATH=`php -r "echo realpath('$DRUSH_BIN');"`
+
 DRUSH_DIRNAME="`dirname -- "$DRUSH_PATH"`"
 
 RUNNER="$DRUSH_DIRNAME/vendor/phpunit/phpunit/phpunit.php"
@@ -23,7 +26,7 @@ fi
 echo "Using phpunit at $RUNNER"
 
 if [ $# = 0 ] ; then
-  $RUNNER --bootstrap="$DRUSH_DIRNAME/tests/drush_testcase.inc" .
+  $RUNNER --bootstrap="$DRUSH_DIRNAME/tests/bootstrap.inc" .
 else
-  $RUNNER --bootstrap="$DRUSH_DIRNAME/tests/drush_testcase.inc" "$@"
+  $RUNNER --bootstrap="$DRUSH_DIRNAME/tests/bootstrap.inc" "$@"
 fi
