diff --git a/includes/project_src_github.archive.inc b/includes/project_src_github.archive.inc
index 72a5db9..15856e4 100644
--- a/includes/project_src_github.archive.inc
+++ b/includes/project_src_github.archive.inc
@@ -24,7 +24,7 @@ function project_src_github_get_archive($namespace, $project_name, $version) {
   // Provide a shortcut for -dev releases (which should never be saved to the
   // local file system).
   if (substr($version, -1) == 'x') {
-    drupal_add_http_header('Content-type', 'application/octet-stream');
+    drupal_add_http_header('Content-type', 'application/x-gzip');
     drupal_add_http_header('Content-transfer-encoding', 'binary');
     drupal_add_http_header('Content-disposition', 'attachment; filename="' . $project_name . '-' . $version . '.tar.gz"');
 
@@ -61,7 +61,7 @@ function project_src_github_get_archive($namespace, $project_name, $version) {
   }
 
   if ($success) {
-    drupal_add_http_header('Content-type', 'application/octet-stream');
+    drupal_add_http_header('Content-type', 'application/x-gzip');
     drupal_add_http_header('Content-transfer-encoding', 'binary');
     drupal_add_http_header('Content-disposition', 'attachment; filename="' . $project_name . '-' . $version . '.tar.gz"');
 
@@ -102,7 +102,7 @@ function project_src_github_get_archive($namespace, $project_name, $version) {
  * @todo We should really be storing/managing tarballs after repackaging.
  */
 function _project_src_github_repackage_tarball($namespace, $project, $version, $managed_destination) {
-  $github = 'https://github.com';
+  $github = variable_get('project_src_github_domain', 'https://github.com');
   $extension = $namespace . '/' . $project;
   $get = $github . '/' . $extension . '/archive/' . $version . '.tar.gz';
   $tarball = _project_src_github_file_as_string($get);
diff --git a/includes/project_src_github.private.inc b/includes/project_src_github.private.inc
index 7c26a50..8ac4d78 100644
--- a/includes/project_src_github.private.inc
+++ b/includes/project_src_github.private.inc
@@ -21,8 +21,9 @@
  *   A release array suitable for use in the primary release theme function.
  */
 function _project_src_github_format_release_info($commitref, $info) {
-  // Root of the project on GitLab.
-  $root = 'https://github.com/' . $info['api']['path_with_namespace'];
+  // Root of the project on GitHub.
+  $github = variable_get('project_src_github_domain', 'https://github.com');
+  $root = $github . '/' . $info['api']['path_with_namespace'];
 
   // Filter $commitref['name'] here, since it's used very often below.
   $commitref['name'] = check_plain($commitref['name']);
diff --git a/project_src_github.info b/project_src_github.info
index 82deef7..f42b73a 100644
--- a/project_src_github.info
+++ b/project_src_github.info
@@ -5,3 +5,5 @@ package = Development
 configure = admin/config/development/github-src
 dependencies[] = project_src
 dependencies[] = github_api
+
+files[] = project_src_github.test
diff --git a/project_src_github.install b/project_src_github.install
index 13680fb..dae139f 100644
--- a/project_src_github.install
+++ b/project_src_github.install
@@ -12,6 +12,7 @@
 function project_src_github_uninstall() {
   $variables = array(
     'project_src_github_org',
+    'project_src_github_domain',
   );
 
   foreach ($variables as $variable) {
diff --git a/project_src_github.test b/project_src_github.test
new file mode 100644
index 0000000..d8ec85f
--- /dev/null
+++ b/project_src_github.test
@@ -0,0 +1,431 @@
+<?php
+
+/**
+ * @file
+ * SimpleTest tests for the Project Source: GitHub module.
+ */
+
+
+/**
+ * Helper class with some added functions and properties for testing.
+ */
+class ProjectSrcGithubBaseCase extends DrupalWebTestCase {
+
+  /**
+   * The GitHub organization used for testing.
+   */
+  protected $org = 'drewpaul_inc';
+
+  /**
+   * @var GithubApiMock
+   */
+  protected $github;
+
+  function setUp(array $modules = array()) {
+    $modules[] = 'project_src';
+    $modules[] = 'project_src_github';
+    parent::setUp($modules);
+
+    // Ensure the test environment has the proper public files directory.
+    variable_del('file_public_path');
+
+    // Order matters!
+    $fake_api = 'project_src_github_test_github_api';
+    $this->fakeModuleUninstall('composer_manager');
+    $this->fakeModuleUninstall('github_api');
+    drupal_flush_all_caches();
+    $this->fakeModuleInstall($fake_api);
+
+    // Set relevant Project Source: GitHub variables.
+    variable_set('project_src_github_org', $this->org);
+    $module = drupal_get_path('module', $fake_api);
+    $github = $GLOBALS['base_url'] . '/' . $module . '/archives';
+    variable_set('project_src_github_domain', $github);
+
+    // Keep track of our fake GitHub API object for test comparisons.
+    module_load_include('module', $fake_api, $fake_api);
+    $this->github = new GithubApiMock();
+  }
+
+  /**
+   * Performs an unofficial module install (in that no hooks are run and no
+   * dependencies are loaded or installed).
+   *
+   * @param string $module
+   *   The module to "install."
+   */
+  protected function fakeModuleInstall($module) {
+    db_update('system')
+      ->fields(array('status' => 1))
+      ->condition('type', 'module')
+      ->condition('name', $module)
+      ->execute();
+
+    system_list_reset();
+    module_list(TRUE);
+    module_implements('', FALSE, TRUE);
+    _system_update_bootstrap_status();
+    registry_update();
+    drupal_get_schema(NULL, TRUE);
+    drupal_theme_rebuild();
+    entity_info_cache_clear();
+  }
+
+  /**
+   * Performs an unofficial module uninstall (in that no hooks are run and no
+   * dependencies are loaded or installed).
+   *
+   * @param string $module
+   *   The module to "uninstall."
+   */
+  protected function fakeModuleUninstall($module) {
+    db_update('system')
+      ->fields(array('status' => 0))
+      ->condition('type', 'module')
+      ->condition('name', $module)
+      ->execute();
+
+    system_list_reset();
+    module_list(TRUE);
+    module_implements('', FALSE, TRUE);
+    entity_info_cache_clear();
+    registry_update();
+    _system_update_bootstrap_status();
+    drupal_theme_rebuild();
+  }
+}
+
+
+/**
+ * Tests Project Source: GitHub XML generation.
+ */
+class ProjectSrcGithubXMLTests extends ProjectSrcGithubBaseCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Project Source GitHub - XML',
+      'description' => 'Tests the Project Source GitHub XML generation.',
+      'group' => 'Project Source GitHub',
+    );
+  }
+
+  /**
+   * Compares the provided project XML values with those provided by our mock
+   * GitHub API object.
+   *
+   * @param SimpleXMLElement|array $xml
+   *   The SimpleXMLElement as returned by the xpath query (or an array that
+   *   contains at a SimpleXMLElement as its first item).
+   * @param string $api
+   *   The API version of the requested project XML.
+   */
+  protected function validateProjectXml($xml, $api) {
+    $repos = $this->github->repositories($this->org);
+    $repo = $repos[$this->org . '_module'];
+
+    // We may be handed an array.
+    if (is_array($xml)) {
+      $xml = $xml[0];
+    }
+
+    $major_version = substr($repo['default_branch'], 4, 1);
+
+    $this->assertEqual($xml->title, $repo['name'], format_string('Returned the expected project title %arg.', array('%arg' => $xml->title)));
+    $this->assertEqual($xml->short_name, $repo['name'], format_string('Returned the expected project short name %arg.', array('%arg' => $xml->short_name)));
+    $this->assertEqual($xml->creator, $repo['owner']['login'], format_string('Returned the expected project creator %arg.', array('%arg' => $xml->creator)));
+    $this->assertEqual($xml->api_version, $api, format_string('Returned the expected API version %arg.', array('%arg' => $xml->api_version)));
+    $this->assertEqual($xml->default_major, $major_version, format_string('Returned the expected default major version %arg.', array('%arg' => $xml->default_major)));
+    $this->assertEqual($xml->project_status, 'published', 'Returned the expected status.');
+    // @todo Recommended/supported major?
+    // @todo Link?
+  }
+
+  /**
+   * Compares the provided release XML values with those provided by our mock
+   * GitHub API object.
+   *
+   * @param SimpleXMLElement $xml
+   *   The SimpleXMLElement as returned by the xpath query.
+   */
+  protected function validateReleaseXml($xml) {
+    $repos = $this->github->repositories($this->org);
+    $repo = $repos[$this->org . '_module'];
+    $branches = $this->github->branches($this->org, $this->org . '_module');
+    $commit = $this->github->show($this->org, $this->org . '_module', '');
+
+    // Tests if we're dealing with a dev release.
+    if (substr($xml->version, -4) == '-dev') {
+      $expected_extra = 'dev';
+      $real_tag = substr($xml->version, 0, -4);
+    }
+    // Test if we're dealing with an unstable release.
+    elseif (preg_match_all('/.*?((unstable|alpha|beta|rc)\\d+)/is', $xml->version, $matches)) {
+      $expected_extra = $matches[1][0];
+      $real_tag = $xml->version;
+    }
+    else {
+      $expected_extra = NULL;
+      $real_tag = $xml->version;
+    }
+
+    $name = $this->org . '_module ' . $xml->version;
+    $major_version = substr($real_tag, 4, 1);
+    $github = variable_get('project_src_github_domain', 'https://github.com');
+    $release_link = $github . '/' . $repo['full_name'];
+    $file_name = 'project-src-github/' . $repo['full_name'] . '/' . $real_tag . '/' . 'download';
+
+    $this->assertEqual($xml->name, $name, format_string('Returned the expected release name %arg.', array('%arg' => $xml->name)));
+    $this->assertEqual($xml->version, $xml->tag, format_string('Returned the expected release tag %arg.', array('%arg' => $xml->tag)));
+    $this->assertEqual($xml->version_major, $major_version, format_string('Returned the expected major version %arg.', array('%arg' => $xml->version_major)));
+    $this->assertTrue(strpos($xml->download_link, $file_name) !== FALSE, format_string('Returned the expected release download link %arg.', array('%arg' => $xml->download_link)));
+    $this->assertEqual($xml->date, strtotime($commit['commit']['author']['date']), format_string('Returned the expected release date %arg.', array('%arg' => $xml->date)));
+    $this->assertEqual($xml->release_link, $release_link, format_string('Returned the expected release link %arg.', array('%arg' => $xml->release_link)));
+    $this->assertEqual($xml->status, 'published', 'Returned valid status.');
+
+    // If this release corresponds to a dev/alpha/beta/etc. release, test it.
+    if (!empty($expected_extra)) {
+      $this->assertEqual($xml->version_extra, $expected_extra, format_string('Returned expected extra version metadata %arg.', array('%arg' => $xml->version_extra)));
+
+      // Ensure that dev tags correspond to actual branches from the API.
+      if ($expected_extra == 'dev') {
+        $this->assertTrue(isset($branches[$real_tag]), format_string('Release %arg matches a valid branch.', array('%arg' => $real_tag)));
+      }
+    }
+  }
+
+  /**
+   * Compares the provided file XML values with those provided by our mock
+   * GitHub API object.
+   *
+   * @param SimpleXMLElement $xml
+   *   The SimpleXMLElement as returned by the xpath query.
+   */
+  protected function validateFileXml($xml) {
+    $repos = $this->github->repositories($this->org);
+    $repo = $repos[$this->org . '_module'];
+    $commit = $this->github->show($this->org, $this->org . '_module', '');
+
+    $version = '';
+    if (preg_match_all('/.*?(\\d+\\.x-\\d+\\..(-.*\\d+)?).*?/is', $xml->url, $matches)) {
+      $version = $matches[1][0];
+      $this->verbose('<pre>' . print_r($matches, TRUE) . '</pre>');
+    }
+    elseif (preg_match_all('', $xml->url, $matches)) {
+
+    }
+    $file_name = 'project-src-github/' . $repo['full_name'] . '/' . $version . '/' . 'download';
+
+    // Note File size and md5 are validated separately.
+    $this->assertTrue(strpos($xml->url, $file_name) !== FALSE, format_string('Returned the expected release download link %arg.', array('%arg' => $xml->url)));
+    $this->assertEqual($xml->archive_type, 'tar.gz', 'Returned the expected archive type.');
+    $this->assertEqual($xml->filedate, strtotime($commit['commit']['author']['date']), format_string('Returned the expected file date.', array('%arg' => $xml->filedate)));
+  }
+
+  /**
+   * Main XML generation test method.
+   */
+  public function testProjectSourceProjectXml() {
+    // Check for valid XML at 6.x.
+    $this->drupalGet('drupal/release-history/' . $this->org . '_module/6.x');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['content-type'], 'text/xml; charset=utf-8', 'Found valid XML for 6.x.');
+    $this->validateProjectXml($this->xpath('//project'), '6.x');
+    $this->assertNoFieldByXpath('//version', 'master', 'No release found for "master."');
+
+    // Check 6.x releases.
+    $releases = $this->xpath('//release');
+    $this->assertEqual(count($releases), 2, 'Found 2 release for 6.x.');
+    foreach ($releases as $release) {
+      $this->validateReleaseXml($release);
+    }
+
+    // Check 6.x files.
+    $files = $this->xpath('//file');
+    $this->assertEqual(count($files), 2, 'Found 2 file for 6.x');
+    foreach ($files as $file) {
+      $this->validateFileXml($file);
+    }
+
+    // Check for valid XML at 7.x.
+    $this->drupalGet('drupal/release-history/' . $this->org . '_module/7.x');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['content-type'], 'text/xml; charset=utf-8', 'Found valid XML for 7.x.');
+    $this->validateProjectXml($this->xpath('//project'), '7.x');
+    $this->assertNoFieldByXpath('//version', 'master', 'No release found for "master."');
+
+    // Check 7.x releases.
+    $releases = $this->xpath('//release');
+    $this->assertEqual(count($releases), 5, 'Found 5 releases for 7.x');
+    foreach ($releases as $release) {
+      $this->validateReleaseXml($release);
+    }
+
+    // Check 7.x files.
+    $files = $this->xpath('//file');
+    $this->assertEqual(count($files), 5, 'Found 5 files for 7.x');
+    foreach ($files as $file) {
+      $this->validateFileXml($file);
+    }
+
+    // Check that there are no results for 8.x.
+    $this->drupalGet('drupal/release-history/' . $this->org . '_module/8.x');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['content-type'], 'text/xml; charset=utf-8', 'Found valid XML for 8.x.');
+    $this->assertNoFieldByXpath('//release', NULL, 'No releases found for 8.x.');
+    $text = 'No release history was found for the requested project (' . $this->org . '_module).';
+    $this->assertFieldByXPath('//error', $text, 'Found "no release history" messaging for 8.x');
+  }
+}
+
+
+/**
+ * Tests Project Source: GitHub file generation.
+ */
+class ProjectSrcGithubFileTests extends ProjectSrcGithubBaseCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Project Source GitHub - File',
+      'description' => 'Tests the Project Source GitHub file generation.',
+      'group' => 'Project Source GitHub',
+    );
+  }
+
+  /**
+   * Validates file metadata and validity.
+   *
+   * @param array $commitrefs
+   *   An array of commit refs, keyed by ref (e.g. tag name or branch name).
+   * @param bool $file_exists
+   *   Checks whether or not the file exists and is managed after requesting it.
+   */
+  protected function validateFileMetadata(array $commitrefs, $file_exists) {
+    $root = 'public://project-src-github/drewpaul_inc/drewpaul_inc_module';
+    $module = $this->org . '_module';
+
+    foreach ($commitrefs as $ref => $info) {
+      $file = $root . '/' . $ref . '/download.tar.gz';
+
+      // Check that the file does not exist locally.
+      $this->assertFalse(file_exists($file), format_string('No local file for !ref yet.', array(
+        '!ref' => $ref,
+      )));
+
+      // Then get the file and check the response, content type, and filename.
+      $this->drupalGet(file_create_url($file));
+      $headers = $this->drupalGetHeaders();
+      $this->assertEqual($headers[':status'], 'HTTP/1.1 200 OK', 'File returned a 200.');
+      $this->assertEqual($headers['content-type'], 'application/x-gzip', format_string('Found a valid tar archive for !ref.', array(
+        '!ref' => $ref,
+      )));
+      $filename = $module . '-' . $ref . '.tar.gz';
+      $this->assertEqual($headers['content-disposition'], 'attachment; filename="' . $filename . '"', format_string('File name header set to !filename', array(
+        '!filename' => $filename,
+      )));
+
+      // If desired, check that the file now exists.
+      if ($file_exists) {
+        // Then check that the file does exist locally.
+        $this->assertTrue(file_exists($file), format_string('File successfully saved for !ref.', array(
+          '!ref' => $ref,
+        )));
+
+        // Check that the file is being managed.
+        $managed_file = _project_src_github_load_file_by_uri($file);
+        $this->assertEqual($managed_file->filename, 'download.tar.gz', 'Found a valid entry in the managed file system.');
+
+        // Clean up the file.
+        file_delete($managed_file);
+      }
+      else {
+        // Ensure a file was not created.
+        $this->assertTrue(!file_exists($file), format_string('File was not saved for !ref.', array(
+          '!ref' => $ref,
+        )));
+
+        // Ensure no managed file entry was created.
+        $this->assertFalse(_project_src_github_load_file_by_uri($file), 'Found no entry in the managed file system.');
+      }
+    }
+  }
+
+  protected function validateFileInfoMetadata(array $commitrefs) {
+    $project = $this->org . '_module';
+    $root = 'public://project-src-github/' . $this->org . '/' . $project;
+    $show = $this->github->show($this->org, $project, '');
+    $date = $show['commit']['author']['date'];
+
+    foreach ($commitrefs as $ref => $info) {
+      $get = $root . '/' . $ref . '/download.tar.gz';
+
+      // Test a managed file.
+      $tarball = file_get_contents(file_create_url($get));
+
+      // Save the tarball to the temp directory.
+      $tar_name = md5($get) . '.tar.gz';
+      $tar_file = file_unmanaged_save_data($tarball, 'temporary://' . $tar_name);
+
+      // Get an archiver and extract the GitHub tarball into its own directory.
+      $tar_extracted = 'temporary://' . $tar_name . '.extract';
+      file_prepare_directory($tar_extracted, FILE_CREATE_DIRECTORY);
+      $tar = archiver_get_archiver($tar_file);
+      $tar->extract($tar_extracted);
+      $source = $tar_extracted . '/' . $project;
+
+      // Extract the info file metadata.
+      $info_file = $source . '/' . $project . '.info';
+      $this->assertTrue(file_exists($info_file), 'Found the project info file.');
+      $info = drupal_parse_info_file($info_file);
+
+      // Check all relevant info file properties.
+      $expected_url = $GLOBALS['base_url'] . '/drupal/release-history';
+      $this->assertEqual($info['project status url'], $expected_url, format_string('Project status URL !url included in .info file.', array(
+        '!url' => $expected_url,
+      )));
+      $this->assertEqual($info['project'], $project, format_string('Project !project included in .info file.', array(
+        '!project' => $project,
+      )));
+      $this->assertEqual($info['version'], $ref, format_string('Version !version included in .info file.', array(
+        '!version' => $ref,
+      )));
+      $this->assertEqual($info['datestamp'], strtotime($date), format_string('Date !date included in .info file.', array(
+        '!date' => $date,
+      )));
+
+      // Clean up all remaining temporary files.
+      file_unmanaged_delete($tar_file);
+      file_unmanaged_delete_recursive($tar_extracted);
+      if ($managed_file = _project_src_github_load_file_by_uri($get)) {
+        file_delete($managed_file);
+      }
+    }
+  }
+
+  /**
+   * Tests tagged and branch-based release tar archive metadata.
+   */
+  public function testProjectSourceProjectFiles() {
+    // Ensure that all tagged releases have valid archives.
+    $tags = $this->github->tags($this->org, $this->org . '_module');
+    $this->validateFileMetadata($tags, TRUE);
+
+    // Ensure that all branches have valid archives.
+    $branches = $this->github->branches($this->org, $this->org . '_module');
+    unset($branches['master']);
+    $this->validateFileMetadata($branches, FALSE);
+  }
+
+  /**
+   * Tests release .info packaging rewriting.
+   */
+  public function testProjectSourceProjectInfoMetadata() {
+    // Ensure all tagged releases' archives have properly packaged .info files.
+    $tags = $this->github->tags($this->org, $this->org . '_module');
+    $this->validateFileInfoMetadata($tags);
+
+    // Ensure all branch releases' archives have properly packaged .info files.
+    $branches = $this->github->branches($this->org, $this->org . '_module');
+    unset($branches['master']);
+    $this->validateFileMetadata($branches, FALSE);
+  }
+}
diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/6.x-1.0.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/6.x-1.0.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..d77c7955195ee7e6d87cdc291308929d4625dffb
GIT binary patch
literal 829
zcmV-D1H$|tiwFP!000001MQe$Z`(EyhVz-f;#LgHnzgbeTN0Yh=!zxiQlMSYb^`_@
z5Gd&+bCyVrq~aPw|9ekTytZzfAe+}=D<2r3MBN=vBJU;PhQChAN~OF?7RYnd2m5DC
zL{ZeY#CA&$rw4uzOyXz~M&p?J{b?A42jxGI%SW`va!o^OrR;TfH$C5ex&H`Z0GX<5
zly(+i5JWhMrkI6Ukv-^saX5~-{{V|P@`Iv4KaB$RZ|CgTAGyX=!`mtmGD~Wew-w%x
zyw~@AZ#>%97+pQYak#tw>3XaFqp=?!g!@Y1IzIFIzi$3toIW~vdg|5r-yCDm|LLyz
z_s5fHd;a4fru||sZ+HWr`TW0HAe_(|fakOb;7n#7JndHjo9%?LACxRLw{69(P||z>
zzpD07BNzn**O04~1WS=MPg7xGp(><%ZedM!7YMDSkFGKAj2K<g@uLD&04Mh)!mti|
zk3$1Ba%sUTNXgNV6E|eI;_I%@qOo+Rkyq?Qw+*j0-s!qeLGl{A(DUb7{f3!^m#Wnn
z9>Zf{e{9q9`MJl~Pw9nFt*O@c$VZq>h)hwQLK=_?R!T30EU7O?51uJNZY*lRvNZSL
z?63x2XfAS(J)tEvgsFsFc1arKI$OYFrAmr=R`U`K6iUOL`FmfeOXUt@^X4*9bD3x<
z?1B<KhK03_IUbGj{yjaV%h9FnrcI2_lEI#R5c-|-YPIspJ`s;L{?U-y!{29v^*5dM
zS2nF@!ek4#)Bo?ePXDiX<_TG-+m3OA{*NdA_W3^uVt?ZF{}a&5;|z9h138*Zi>A|g
zvU~J_K=*1)I(xOWeyr0QrPtW-Y{^T~Q;%gzcNx&4V?%Fu_lv-lp|Km3Im%3+nV(bl
z#ns&nmslC6R)2xp*8kAlZj77jKisbWNicQizfZw;51U29*y!kp!4Z642r7BH7>b|H
z4r)n7EORJYne}JA{@h}B$%RMGBlc&!v%0CU`*Xqcp%P(|Mx(XsBvY#T`tD%9i49jQ
zxNVm&g^@N9_AdOvfP}=hTEeRrKc9Zf-Z%kpI2;a#!{Kl^91e%W;cz${j(5BTbk~t-
H04M+e1N5k=

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/6.x-1.x.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/6.x-1.x.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..eff2e228e344c59272e50ce10dc84d3e8a433eb7
GIT binary patch
literal 827
zcmV-B1H}9viwFP!000001MQe$Z`(EyhVz-f;#LgHnzgbmTN0Yh=!zxiQlMMWbpr+?
z5NPTobCyVrq~aJu|NBl-ysl21Ae+}=D<23TMco}w7VjnDnqQ_xspq^*7sxX-2m5D8
z#BtoU#CA&$CkLZ294GNOiiZh}k0wzV9TfjSF4t(R<A$a*TD#lq?s~rca{m#c5QMHO
zRBjq#7{)k`CnzG3izn@sM8kxSo?xEDqcG2LG>=2}Z|CgUADO{b&6_fnN~D#}ni3z!
z!R6yoa5>o59Bn-$NwmBEslQeKwAzw`XkQ83#z$WNw|)QDr_WAao(5I+H|OZ}f3oZQ
zkA~xT`}-%8A?+7?dBZ#S$iM%a1;Pof0eD4=0M3*M;AOW8*mNh1eWO)jxNA!8q*nG5
z_*plH2El5`xq(ct6gY}(_<SxMEOd!<&mFAE?gF7v^rIWhS|dhXI)0R(bKvB@MCjLH
z=W%GELarP*4Rdm|<isr*uK2ngb7&pi8RR89F-^_OjrV-rrJ#6)ZRpu6qklo+;0N6p
zfyeM%x^J8L`RqJk?ECqJ)Qv6IkH|;bO$ebWPpK>@4J&PyQWZ3oVF2H0KyDoxz@o5^
z;Owvho*OQ+fW4q4)rTpiTDD1A<VGyuxz+_mJ*#+u7IJOi!R$}JP@BpF#%A?ZqV_7$
zLb?SddJGHaYI{5wWZioP+7yE;+fAEVBhucUy%+kO^J=vUiY}3WHvU1M+S6aBz4aHZ
z^=EEgO{EnJztjKkxlR9Xcos;N>-)}ehyD-8qwVv5IHt?%{|BIx$0_XI1~Rn5$hy^e
zvU~QPKzC|PI(xIUUf1c3(i^O~Sn`7OG+;uTHUnC8tRHRfev-JdG<S_ELnS2I**Ohg
z+&o-!g{Ads^%uBr{rAoN=D555qigj~JzxJHfUlm`i<+^)(Gi0q_&S$V^3>^zpUw_?
zNkyzO$QvcPvtD;@al7OqAm;)5Bi>n6m)QQfV7pMMv}tY7xpgYEE<b<Rn{Q&n4GV7D
z<x^>uOQm}VzcC;oag9;%=Jk)KU$VDe06ZR#$K&yMJRXn7<MDVr9*^f8zXO{00Z9NT
F006!9r8WQn

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-1.0.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-1.0.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..0853786737b190be770634d7df8f24af6f9ca664
GIT binary patch
literal 829
zcmV-D1H$|tiwFP!000001MQe$Z`(EyhVz-f;#LgHnzgbm*>aH0=!zw1GoW43bpr+?
z5NPr&bCyVrq~aRI{(DbSysmDXAe+}=D<2q;MBN=v;@vC46@Q)Og_-dpTOgNc5B5)=
zNYk`E65B_*IXoD|aWYPm(RkR54~C;SImrKkTt1+7o?DvI8sl%WyY2q=!~I7X#UM;s
zqV^LZ#4)W?ERKisc$6f^!z7k{$;Zb@oDO7~#3(V1CHuE?4(tzUaaHlU$dndYX=GjC
z!*TTb;UMbw_BBV-599G@cmLD*R{y8;I~eUNft&cm`~Rl(e|h%!^x0Wd%D*{Br~kuU
z>p$ox>Gt}M<ECEr@`ShWiLd{w1;Q!S0KA|gfO9P(c-B?{o9u+K?~KkZ_jSR&GTMCx
zznJ>aA~*wcZb6!r22YVKpUsqqg(;A(xra5`T_V()Ub;ou7%@7f?MDG-4xHSV2wfev
z9)}J}<l2KbFe67tPTY~<im#h7kIvJTMP9H|TUWf;c+b{t3YwSLgr2^z=2sLRel)ce
zcmhw9e_GEjrWX-oKg=$bsa>&tKt9TCLI^{7D(yfUSQ)!iI;Sy-5j;15+<CNsdF~#-
z`C$osZn=^X`<_aw3sWe)Y?5@ytysVlV{(dmUh*6r%#DS6(?7kTCY5`PO{=R!-BqHw
z@(W7z1Qy;`?xfd~?Q=%P=DjQ1%`tOUWSu>GFZ4U-)oK;xZ6XnU_<LPykA9nU)?YQ&
zU-(%$QBEwvm;QgxP5OV$y-4Y~x$7La=zl*MZ14Z$csz)M{(l5od7QxR&wxZHtg0HF
zC%eb*33RK*q_fvc>xVkMQF@CN7fYU#o<>X<+hjmR$GXwx>Su*3M{`%GC2FD2O)qHp
z^7`S5Yb;z)tG~cq`@cKfZI0Xff3)5INjwbuzmLH;kE%t*SnueF!4Z5rSJd)!(ltNr
z9n6xNSWB4KTC{t;cHiQ6$wfrYJ@!Yu^Rg<i`E$Xwp)%#N%A)t{Oc+yqdA~E?#D;4M
zZrkN^<+RU~zYlL1kdXM=YIybXr?aow??C_pfj}S-2m}IwKp+qZ1OkCT;2m!PnVjta
H04M+eJrt=@

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-1.x.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-1.x.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..8c617be9aa58b2e275dd9f40f762e9bd2bc4b965
GIT binary patch
literal 828
zcmV-C1H=3uiwFP!000001MQe$Z`(EyhVz-f;#LgHnzgbm*>aH0=!zxiGN4=0bpr+?
z5NPr&bCyVrq~aLE{`*c+ysl21Ae+}=E1xfvsJr9Iyn98s;#XN-m>Dm!1#*e@VE^=q
zG)>!)*dFQO@L&+f$v90$<6$#D7>?rPApZw)xk2qbx3r`+#@}Uk-}CL4`;Rb+L71{c
z?I%KrW7?-!91rL5C`pcoNi6%4kB^f$9mq6^QDPcP_HXAL*dNm3s^WE#DJ`<n$hyF%
z<LK(?AiC=9YmKHK#^cfM{-^7${!a&SdNA5o0(bF&_y1k{|N89t>C3aIlz(%LPXC9y
z_J7b%((V1Hf$C*1Z+H(M`2N3HAe>SSz$+>OIM*VAmu(fW$xayi%IMs3Ul-gfquodF
zlc^6af-^AZ7Nl8e@D$nd*-Uv@m;&jZdsvg*B|@#~N4F>&BSx2W{3yW8fs^|Zp{v8z
z<Iq8gTzl{aX5{F|i90e}@pUuj(RsSF$P0FA>xvf}@7cOdLGu!u(9>7e{EWiG_olW2
zPvC{}U)QsX=|#lYce6`nYFDhEkdJbk5W-NNN;}X7R>m%s&S_3!1m7A!?mSw+Ja<pv
z{ICQ*w_M4HeM2SHg(;L?Hc2|<RxIF!F*!v&FL{m*=ElOK>7Ra~CY48wO{?of-F2e5
z@(W7z1Qy;`?xfd~?R!SX=Dlm%&6qhWvd*5p6Z)<5YPE{;Hj#)v{Jk!<XTMB3>(3hN
zPyDQ$C?^)-OaH&;F8#mdS)}ybJamnF^uM1Bw$J}@(vRby|L=iT9w)H-Ga%6ktExul
z$?o|(0^O=H>Fn*&dQ+!2N^h~^V##yT(})RUn+&MvSU20;{itx|XzdEML@gA$=><(+
z-acJ%jfD$p^%r<(|98W~*0{g_M>qOE8HMxT`{0Xb)uLjocXY(y2)>*vYI(ZonxD=N
zW=Tz~CCqCr+OuAJZt=V1A|mG=`y<|YSryp)x!~GRnQ~cW(ff5Kj43{S+?j7;!z~53
z?eej5+Gol?hF=+wkoekac=P&)v(MRYK>z}QKp+qZ1OkCTAP@)y0)ar_Ex!YIynP@5
GC;$N0B)K2}

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-2.0-alpha1.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-2.0-alpha1.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..7362ca0aa7ad31f89767f25ec0f374927a04f3a7
GIT binary patch
literal 834
zcmV-I1HJqoiwFP!000001MQe$Z`(EyhW(kp;#v$%n>Dg6$#Rg*Xp1IjQ=nbZb^`_@
z5NMK?I7_5TQgMx<|Gg(EURx(#f-K9hmCqMS^xg3!-n}AR(GO`>=ou~2Tu?4d@8%gG
zNs_c9vN__9ac>xg(Kv}l<G7h0#-lLmW&c1f=cuit2A4EiyT7u#?Dx%w`;RaRf$6do
z%1sz!`?yab+mGkrD2n#uDC7f9$NN#340#fTf{P^N<loNOb8~PbmKCjwR4SI1n%9Nc
z8wVfu!eFpRiz=tX{@-4s>4))nw7viFdZYix!zAjBZs7SY<vZ{HEA0Q9qbG+ikAjk4
zca2W}<8Avt97M^+{)dA&4tuxq62J5Pf0qk5#2SFtSOjpaSO72EDj<`s5b{*3%urVs
z)Jd)E9r#_>J4S%jFsBB%UMg@H+0fZcI>>b)@SZwYq1~x~TH#AKf;UD4FY)+MfSv<I
z_l1D24qJ~M3#FjSfzvQUM~hC>qT!ORnmH$|!#g8rK@Lq_(PHgATeT@DT8bw0^tI7{
z2<G6et_>3h@Jzbr_3UJN5)kss>{RO77ORKoBkei_)0n4J7L<miHVdgToZ}*ZR~iJh
zP8bkbW*@@wP6>2wsN?~8fhE<2DWqC7Nm@Y-%i)>U8Ad%WX(lYpwSl|Suij9T%3VUH
z)mftUEYVE591}f&+_}mg^!vPh&On>2e`dQLQ)^h-*^_TVe{o(emqFGh65xlw-=+5G
z{iL)0v9Z4AX5~a$misUL|DG%8{{<$2RC9ghH7?QrK{VVv|A+BlH1_&`3$*e$f$g6G
zE-W*$YIGj$o_r(Fts0}wUM#KWb$YGzMpTq7Xoh+k5T;F&0TvzUW}Ca;q*z*9yAp~E
z#iX#)6P!N1c)FrW6xOTNHMnN~cf%FexV-;I=lVY$`t#py@bjZ8uL$Yy?h@FA$8(7-
zj~89@<Jmzku!$9id97G`)@#o#Zkt>L=-elN#yc*nLNtFa*fvxuZCV-O+$v>S7xy1@
z=IhvSL8WcF{2;AzsdNwEBLNf=R~rTI-u!y>6Zzx?z~k|FJRXn7<MDVr9*@W4@p!)Q
M8Oe&|hyW-609hNSX8-^I

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-2.0.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-2.0.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..485ee7fa7c679766344cc8bb608fc96d0f166d4a
GIT binary patch
literal 839
zcmV-N1GxMjiwFP!000001MQe$Z`(EyhW(kp;#L$(o29ZP+j5f4=!zxiGN4=0bpr+?
z5Gd*_bCyVrq~aRI{`*c+ysd7WAe+}=E1xfv$h+f7<h`O@@i$3c=#&@90(pj}w|@pi
z9LH@-Y_~Kx>V;7>oW#R898IV{9F3!KFaHN}d5_vUZfHuQwY$ykuJP@M`;Rb=K<KhW
z<)(2uiqpY~2v5S%@!)uPG8D8UPKKBc#e5QHJUouFu^6#`J7>@S$PBJ3UKfc}A}MuN
z7x-inym=A@gZ{qeXzF1y8Sk!ty56e)co2oX@xBtcjgP$kZ(ILY=g-bwoCjt0H|OZ|
zf3$1;!@)4#UjNCk8Q;qj-oZz{{;wAZXS4_4C2az@P$GaA?Ji)`oiO&5R=MG>F1V9g
z*-zjnT^|?(t6|O!WO}8*QDno@R61Db0_mPRSd-lqLapef8_XIbMwfK_C_vAFllu~(
z+lQ^kfrS#ea^N(i<Y>u>TQXenb<^k2I=VB+3wCDeiWeL2bls+)c!^Eu*-N8;M&aOl
zT^oU?@LamD>-2JV88G%;dL?yji}extNV^FkH03Fk1*Ku7%~GnI`Z5gQTMfvqLj#!S
z_6RNxO5k(DWfrh+XiIfr3aOS&k`}oU3wW+|PEjvPo}-1iHt=Bfr#IB3@_?~fb)Bfa
zPBfQpL5ZHi!nw+x_WN1;oPjoZ|JrubCf10gvuE#w{=<2-S_OHVNI)O{ewW(QU#6Y)
zXN~ozE-k0hiiQ8u|L?g?|8E!zq?+sd&T)tS4~F6P`9F##;n?f{2cVV5DeV3XWN3wv
zRipD{_v{^kZq=A{_GW2)U#B-pZ?NKG$#c@vfC+7y3~1A_uD7}SQR2$d+!d+}m5^v>
zm(+cAb9coR7S^lPU*Nv=-!=D}<Iei0-}+niPhFv}{|~?yPpd`6SpV>l!6AG(msIj}
z(G@?P9rTilSY<G;m1xg;?YYJ6l8b<x`|S647iCpo^XGzXLnYECl|kp$iO{-u^tdzM
z#D*I-+_uX@X_ZT)JA#J>ExO0>mH`QhtBr!!uYNfHoc-#Bz~k|FJRXn7<MDVr9*@W4
R@pyW@-vHP$ged?h004&&y?X!v

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-2.x.tar.gz b/test/project_src_github_test_github_api/archives/drewpaul_inc/drewpaul_inc_module/archive/7.x-2.x.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..fd3a09a3d61a8ad8dcda3c02847dfca81763c5b4
GIT binary patch
literal 838
zcmV-M1G)SkiwFP!000001MQgcZ{su+$NRJYipS~1xUox{rcJj&b)49A4C&x>97w2|
zBFA}6O%prV&bkWmzw?r;!1dNmgB}RZpI_|Q?>)cNe&1NF_;r#OI^{*OK%SxL?VkY=
z$8p;d+bvCwdf{*wP2wmHM-v(kN8{nRm%oQx-l4XR8(Pw6?e4O>Z+`pb{v(WsAaq%x
za?>~+#p&QggeT$XcyJt@M1oGlNrY)6=94(%;qfpVixK;`bN1|y%;2ixb&*IVl2T`N
zflnvF_0wT+-QU+5O+8E|<K6X7{jK_s!zk>H_m#k1eBkwe*Z#jge}4AzJSeljxkjh|
zqh0$S4x)H_|0ko6=J)c3_wa%5|C<HE8Jz)mMTY<`lnCHudkWZeCyafmRc^Se3+|*=
z_9OUF*9Qi{YM65anO-Sy6xr}Jl@1oVKzim5)?{~uP%HY;4Q7oIqb^-P3ea=l<i14c
z&SC3uV4*~=95@XrIa+e!mJC;X-HbW3j-Cwif}NSV;>E^0UAHMHUSbn^_R8p=P&oKb
z*GAweypZmzI=!4-28?~1UP)csVtqtD(r!WsO?gUXL1|cNvy>{Qu?z$FMgwx|&;aJS
zJ%Wpa68PM3nFZ`?I#OMjLaJqxq(yGT0$ymHQ`C!+=V)QB4Lq9t=@)8JdBoVPx=GaD
zB$`XNphQn$;ap`;`~9qa&p?~Je`C986Kh1$*|WDo|KYq^t%AHwB%lv}zf0}e&(qHO
z)5iK^mzGm$#lnB-|M%Rb|F_HqQqA>4*SJUj2T{0v{~tyJy8nCqfB&@dIECGxfefuM
zvTAgm?4G|R(5)Ji&fYGq@9OkM=?zv~EO}0P8Ze<vlK~w%){Qn#KS*3zTDwA(p%N19
z?2?ABZXd3=!oqsB`U^a?{=4R3YusD^RO{RIPd#7%{{Wvqs}>bw{lh~Bhw#N*Qpr=N
zD}K5==p_}g%3xkA(cbmidyCs87Xdl<+3)c#%BsNT&js6tN~BFHgU+oJp>^@elg@k-
z8*Vvp+b)l#RW6b42p$`>=$^nY3`kI1Z4|tD{r&l8>{l-Y9*@W4@pwEQkH_Qjcsw4D
Q$J6Wm2CDdHfB+}}0Jj^tA^-pY

literal 0
HcmV?d00001

diff --git a/test/project_src_github_test_github_api/project_src_github_test_github_api.info b/test/project_src_github_test_github_api/project_src_github_test_github_api.info
new file mode 100644
index 0000000..08068b2
--- /dev/null
+++ b/test/project_src_github_test_github_api/project_src_github_test_github_api.info
@@ -0,0 +1,5 @@
+name = Project Source: GitHub - Mock GitHub API
+description = Mock GitHub API module for Project Source: GitHub. Do not enable.
+core = 7.x
+package = Testing
+hidden = TRUE
diff --git a/test/project_src_github_test_github_api/project_src_github_test_github_api.module b/test/project_src_github_test_github_api/project_src_github_test_github_api.module
new file mode 100644
index 0000000..39cee74
--- /dev/null
+++ b/test/project_src_github_test_github_api/project_src_github_test_github_api.module
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * @file
+ * Classes and functions used to help test the Project Source: GitHub module.
+ */
+
+
+if (!function_exists('github_api_client')) {
+  /**
+   * Returns a mock GitHub API object, similar to what is returned by the GitHub
+   * API module.
+   *
+   * @return GithubApiMock
+   *   A custom mock GitHub API object we use for testing.
+   */
+  function github_api_client() {
+    return new GithubApiMock();
+  }
+}
+
+
+class GithubApiMock {
+
+  /**
+   * Returns an instance of the GithubApiMock object.
+   *
+   * @param string $type
+   *
+   * @return GithubApiMock
+   *   Rather than dealing with multiple mock objects, we set this object up to
+   *   return an instance of itself, which contains all methods we actually use.
+   */
+  public function api($type) {
+    return $this;
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for repositories for a given organization.
+   *
+   * @param string $organization
+   * @return array
+   *   An array representing all GitHub repos for a given organization.
+   */
+  public function repositories($organization) {
+    $module = $organization . '_module';
+    $repos[$module] = array(
+      'id' => 1234567,
+      'name' => $module,
+      'full_name' => $organization . '/' . $module,
+      'owner' => array(
+        'login' => $organization . '_user',
+      ),
+      'html_url' => 'http://example.com/' . $organization . '/' . $module,
+      'default_branch' => '7.x-2.x',
+    );
+
+    return $repos;
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for branches for a given author and repository.
+   *
+   * @param string $username
+   * @param string $repository
+   * @param string $branch
+   *
+   * @return array
+   *   An array representing all branches for a given repository.
+   */
+  public function branches($username, $repository, $branch = NULL) {
+    // Note we're making the branch name the key of the returned array. Although
+    // this does not match up with GitHub's response, we use it for testing.
+    $branches['6.x-1.x'] = array(
+      'name' => '6.x-1.x',
+      'commit' => array(
+        'sha' => '231ed864c57ce2c890230b52f958dd33c65e54f6',
+        'url' => 'http://api.example.com/repos/commits/etc/61',
+      ),
+    );
+    $branches['7.x-1.x'] = array(
+      'name' => '7.x-1.x',
+      'commit' => array(
+        'sha' => '231ed864c57ce2c890230b52f958dd33c65e54f7',
+        'url' => 'http://api.example.com/repos/commits/etc/71',
+      ),
+    );
+    $branches['7.x-2.x'] = array(
+      'name' => '7.x-2.x',
+      'commit' => array(
+        'sha' => '65812be004d5b2701be8099c5886071a4909e775',
+        'url' => 'http://api.example.com/repos/commits/etc/72',
+      ),
+    );
+    $branches['master'] = array(
+      'name' => 'master',
+      'commit' => array(
+        'sha' => '65812be004d5b2701be8099c5886071a4909e774',
+        'url' => 'http://api.example.com/repos/commits/etc/master',
+      ),
+    );
+
+    return $branches;
+  }
+
+  /**
+   * Returns an instance of the GithubApiMock object.
+   *
+   * @return GithubApiMock
+   *   Rather than dealing with multiple mock objects, we set this method up to
+   *   return an instance of itself. Commit methods are also located on this
+   *   object.
+   */
+  public function commits() {
+    return $this;
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for commit details for a given commit on a given repository.
+   *
+   * @param $username
+   * @param $repository
+   * @param $sha
+   *
+   * @return array
+   *   An array representing commit details for a given commitref for a given
+   *   repository.
+   */
+  public function show($username, $repository, $sha) {
+    return array(
+      'commit' => array(
+        'author' => array(
+          'date' => 'November 19, 1978',
+        ),
+      ),
+    );
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for release tags for a given repository.
+   *
+   * @param $username
+   * @param $repository
+   *
+   * @return array
+   *   An array representing release tag details for a given repository.
+   */
+  public function tags($username, $repository) {
+    // Note we're making the tag name the key of the returned array. Although
+    // this does not match up with GitHub's response, we use it for testing.
+    $tags['6.x-1.0'] = array(
+      'name' => '6.x-1.0',
+      'zipball_url' => 'https://api.example.com/repos/zipball/6x10',
+      'tarball_url' => 'https://api.example.com/repos/tarball/6x10',
+      'commit' => array(
+        'sha' => '231ed864c57ce2c890230b52f958dd33c65e54f6',
+        'url' => 'http://api.example.com/repos/commits/etc/6x10',
+      ),
+    );
+    $tags['7.x-1.0'] = array(
+      'name' => '7.x-1.0',
+      'zipball_url' => 'https://api.example.com/repos/zipball/7x10',
+      'tarball_url' => 'https://api.example.com/repos/tarball/7x10',
+      'commit' => array(
+        'sha' => '231ed864c57ce2c890230b52f958dd33c65e54f7',
+        'url' => 'http://api.example.com/repos/commits/etc/7x10',
+      ),
+    );
+    $tags['7.x-2.0-alpha1'] = array(
+      'name' => '7.x-2.0-alpha1',
+      'zipball_url' => 'https://api.example.com/repos/zipball/7x20a1',
+      'tarball_url' => 'https://api.example.com/repos/tarball/7x20a1',
+      'commit' => array(
+        'sha' => '65812be004d5b2701be8099c5886071a4909e774',
+        'url' => 'http://api.example.com/repos/commits/etc/7x20',
+      ),
+    );
+    $tags['7.x-2.0'] = array(
+      'name' => '7.x-2.0',
+      'zipball_url' => 'https://api.example.com/repos/zipball/7x20',
+      'tarball_url' => 'https://api.example.com/repos/tarball/7x20',
+      'commit' => array(
+        'sha' => '65812be004d5b2701be8099c5886071a4909e775',
+        'url' => 'http://api.example.com/repos/commits/etc/7x20',
+      ),
+    );
+
+    return $tags;
+  }
+}
