diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 7a2781ecb..72af4183e 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -14,14 +14,19 @@ namespace Composer\Repository\Vcs; use Composer\Cache; use Composer\Config; +use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\IO\IOInterface; +use Composer\Util\Bitbucket; /** * @author Per Bernhardt */ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface { + /** + * @var Cache + */ protected $cache; protected $owner; protected $repository; @@ -29,6 +34,11 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface protected $branches; protected $rootIdentifier; protected $infoCache = array(); + private $hasIssues; + /** + * @var GitDriver + */ + private $gitDriver; /** * {@inheritDoc} @@ -47,9 +57,14 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getRootIdentifier() { + if ($this->gitDriver) { + return $this->gitDriver->getRootIdentifier(); + } + if (null === $this->rootIdentifier) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository; - $repoData = JsonFile::parseJson($this->getContents($resource), $resource); + $repoData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource, true), $resource); + $this->hasIssues = !empty($repoData['has_issues']); $this->rootIdentifier = !empty($repoData['main_branch']) ? $repoData['main_branch'] : 'master'; } @@ -61,7 +76,11 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getUrl() { - return $this->url; + if ($this->gitDriver) { + return $this->gitDriver->getUrl(); + } + + return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; } /** @@ -69,6 +88,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getSource($identifier) { + if ($this->gitDriver) { + return $this->gitDriver->getSource($identifier); + } + return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier); } @@ -87,24 +110,53 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getComposerInformation($identifier) { + if ($this->gitDriver) { + return $this->gitDriver->getComposerInformation($identifier); + } + if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) { $this->infoCache[$identifier] = JsonFile::parseJson($res); } if (!isset($this->infoCache[$identifier])) { - $resource = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'; - $composer = $this->getContents($resource); - if (!$composer) { - return; + $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/src/'.$identifier.'/composer.json'; + $file = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); + if (!is_array($file) || ! array_key_exists('data', $file)) { + return array(); } - $composer = JsonFile::parseJson($composer, $resource); + $composer = JsonFile::parseJson($file['data'], $resource); if (empty($composer['time'])) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier; - $changeset = JsonFile::parseJson($this->getContents($resource), $resource); + $changeset = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $composer['time'] = $changeset['timestamp']; } + if (!isset($composer['support']['source'])) { + $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier; + + if (array_key_exists($label, $tags = $this->getTags())) { + $hash = $tags[$label]; + } elseif (array_key_exists($label, $branches = $this->getBranches())) { + $hash = $branches[$label]; + } + + if (! isset($hash)) { + $composer['support']['source'] = sprintf('https://%s/%s/%s/src', $this->originUrl, $this->owner, $this->repository); + } else { + $composer['support']['source'] = sprintf( + 'https://%s/%s/%s/src/%s/?at=%s', + $this->originUrl, + $this->owner, + $this->repository, + $hash, + $label + ); + } + } + if (!isset($composer['support']['issues']) && $this->hasIssues) { + $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository); + } if (preg_match('{[a-f0-9]{40}}i', $identifier)) { $this->cache->write($identifier, json_encode($composer)); @@ -121,9 +173,13 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getTags() { + if ($this->gitDriver) { + return $this->gitDriver->getTags(); + } + if (null === $this->tags) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; - $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); + $tagsData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $this->tags = array(); foreach ($tagsData as $tag => $data) { $this->tags[$tag] = $data['raw_node']; @@ -138,9 +194,13 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getBranches() { + if ($this->gitDriver) { + return $this->gitDriver->getBranches(); + } + if (null === $this->branches) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'; - $branchData = JsonFile::parseJson($this->getContents($resource), $resource); + $branchData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $this->branches = array(); foreach ($branchData as $branch => $data) { $this->branches[$branch] = $data['raw_node']; @@ -167,4 +227,76 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface return true; } + + protected function attemptCloneFallback() + { + try { + $this->setupGitDriver($this->generateSshUrl()); + + return; + } catch (\RuntimeException $e) { + $this->gitDriver = null; + + $this->io->writeError('Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your Bitbucket OAuth consumer credentials'); + throw $e; + } + } + + /** + * Generate an SSH URL + * + * @return string + */ + private function generateSshUrl() + { + return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git'; + } + + /** + * Get the remote content. + * + * @param string $url The URL of content + * @param bool $fetchingRepoData + * + * @return mixed The result + */ + protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false) + { + try { + return parent::getContents($url); + } catch (TransportException $e) { + $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem); + + switch ($e->getCode()) { + case 403: + if (!$this->io->hasAuthentication($this->originUrl) && $bitbucketUtil->authorizeOAuth($this->originUrl)) { + return parent::getContents($url); + } + + if (!$this->io->isInteractive() && $fetchingRepoData) { + return $this->attemptCloneFallback(); + } + + throw $e; + + default: + throw $e; + } + } + } + + /** + * @param string $url + */ + private function setupGitDriver($url) + { + $this->gitDriver = new GitDriver( + array('url' => $url), + $this->io, + $this->config, + $this->process, + $this->remoteFilesystem + ); + $this->gitDriver->initialize(); + } } diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 89fdd3902..e6e64df7a 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -28,6 +28,8 @@ class Bitbucket private $remoteFilesystem; private $token = array(); + const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; + /** * Constructor. * @@ -81,9 +83,7 @@ class Bitbucket private function requestAccessToken($originUrl) { try { - $apiUrl = 'https://bitbucket.org/site/oauth2/access_token'; - - $json = $this->remoteFilesystem->getContents($originUrl, $apiUrl, false, array( + $json = $this->remoteFilesystem->getContents($originUrl, self::OAUTH2_ACCESS_TOKEN_URL, false, array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', @@ -93,8 +93,15 @@ class Bitbucket $this->token = json_decode($json, true); } catch (TransportException $e) { - if (in_array($e->getCode(), array(403, 401))) { - $this->io->writeError('Invalid consumer provided.'); + if ($e->getCode() === 400) { + $this->io->writeError('Invalid OAuth consumer provided.'); + $this->io->writeError('This can have two reasons:'); + $this->io->writeError('1. You are authenticating with a bitbucket username/password combination'); + $this->io->writeError('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url'); + + return false; + } elseif (in_array($e->getCode(), array(403, 401))) { + $this->io->writeError('Invalid OAuth consumer provided.'); $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); return false; diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 8d0c423bd..43422eea8 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -129,7 +129,9 @@ class Git //We already have an access_token from a previous request. if ($auth['username'] !== 'x-token-auth') { $token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); - $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + if (! empty($token)) { + $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + } } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index d2ced064f..709dcedd5 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -45,11 +45,11 @@ class ProcessExecutor { if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{(://)(?P[^:/\s]+):(?P[^@\s/]+)}i', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { + if (preg_match('{^[a-f0-9]{12,}$}', $m[2])) { return '://***:***'; } - return '://'.$m[1].':***'; + return '://'.$m[2].':***'; }, $command); $this->io->writeError('Executing command ('.($cwd ?: 'CWD').'): '.$safeCommand); } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ffbe693fc..42026e086 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -245,6 +245,11 @@ class RemoteFilesystem unset($options['gitlab-token']); } + if (isset($options['bitbucket-token'])) { + $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['bitbucket-token']; + unset($options['bitbucket-token']); + } + if (isset($options['http'])) { $options['http']['ignore_errors'] = true; } @@ -569,6 +574,25 @@ class RemoteFilesystem ) { throw new TransportException('Could not authenticate against '.$this->originUrl, 401); } + } elseif ($this->config && $this->originUrl === 'bitbucket.org') { + if (! $this->io->hasAuthentication($this->originUrl)) { + $message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to access private repos'; + $bitBucketUtil = new Bitbucket($this->io, $this->config); + if (! $bitBucketUtil->authorizeOAuth($this->originUrl) + && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message)) + ) { + throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); + } + } else { + $auth = $this->io->getAuthentication($this->originUrl); + if ($auth['username'] !== 'x-token-auth') { + $bitbucketUtil = new Bitbucket($this->io, $this->config); + $token = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']); + $this->io->setAuthentication($this->originUrl, 'x-token-auth', $token['access_token']); + } else { + throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); + } + } } else { // 404s are only handled for github if ($httpStatus === 404) { @@ -671,6 +695,10 @@ class RemoteFilesystem if ($auth['password'] === 'oauth2') { $headers[] = 'Authorization: Bearer '.$auth['username']; } + } elseif ('bitbucket.org' === $originUrl + && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL && 'x-token-auth' === $auth['username'] + ) { + $options['bitbucket-token'] = $auth['password']; } else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php new file mode 100644 index 000000000..b8a79fea7 --- /dev/null +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -0,0 +1,228 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository\Vcs; + +use Composer\Config; +use Composer\TestCase; +use Composer\Util\Filesystem; + +/** + * @group bitbucket + */ +class GitBitbucketDriverTest extends TestCase +{ + /** @type \Composer\IO\IOInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $io; + /** @type \Composer\Config */ + private $config; + /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ + private $rfs; + /** @type string */ + private $home; + /** @type string */ + private $originUrl = 'bitbucket.org'; + + protected function setUp() + { + $this->io = $this->getMock('Composer\IO\IOInterface'); + + $this->home = $this->getUniqueTmpDirectory(); + + $this->config = new Config(); + $this->config->merge(array( + 'config' => array( + 'home' => $this->home, + ), + )); + + $this->rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + ->disableOriginalConstructor() + ->getMock(); + } + + public function tearDown() + { + $fs = new Filesystem; + $fs->removeDirectory($this->home); + } + + /** + * @param array $repoConfig + * @return GitBitbucketDriver + */ + private function getDriver(array $repoConfig) + { + $driver = new GitBitbucketDriver( + $repoConfig, + $this->io, + $this->config, + null, + $this->rfs + ); + + $driver->initialize(); + + return $driver; + } + + public function testGetRootIdentifier() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->any()) + ->method('getContents') + ->with( + $this->originUrl, + 'https://api.bitbucket.org/1.0/repositories/user/repo', + false + ) + ->willReturn( + '{"scm": "git", "has_wiki": false, "last_updated": "2016-05-17T13:20:21.993", "no_forks": true, "forks_count": 0, "created_on": "2015-02-18T16:22:24.688", "owner": "user", "logo": "https://bitbucket.org/user/repo/avatar/32/?ts=1463484021", "email_mailinglist": "", "is_mq": false, "size": 9975494, "read_only": false, "fork_of": null, "mq_of": null, "followers_count": 0, "state": "available", "utc_created_on": "2015-02-18 15:22:24+00:00", "website": "", "description": "", "has_issues": false, "is_fork": false, "slug": "repo", "is_private": true, "name": "repo", "language": "php", "utc_last_updated": "2016-05-17 11:20:21+00:00", "no_public_forks": true, "creator": null, "resource_uri": "/1.0/repositories/user/repo"}' + ); + + $this->assertEquals( + 'master', + $driver->getRootIdentifier() + ); + } + + public function testGetParams() + { + $url = 'https://bitbucket.org/user/repo.git'; + $driver = $this->getDriver(array('url' => $url)); + + $this->assertEquals($url, $driver->getUrl()); + + $this->assertEquals( + array( + 'type' => 'zip', + 'url' => 'https://bitbucket.org/user/repo/get/reference.zip', + 'reference' => 'reference', + 'shasum' => '' + ), + $driver->getDist('reference') + ); + + $this->assertEquals( + array('type' => 'git', 'url' => $url, 'reference' => 'reference'), + $driver->getSource('reference') + ); + } + + public function testGetComposerInformation() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->any()) + ->method('getContents') + ->withConsecutive( + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/src/master/composer.json', false), + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/changesets/master', false), + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/tags', false), + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/branches', false) + ) + ->willReturnOnConsecutiveCalls( + '{"node": "937992d19d72", "path": "composer.json", "data": "{\n \"name\": \"user/repo\",\n \"description\": \"test repo\",\n \"license\": \"GPL\",\n \"authors\": [\n {\n \"name\": \"Name\",\n \"email\": \"local@domain.tld\"\n }\n ],\n \"require\": {\n \"creator/package\": \"^1.0\"\n },\n \"require-dev\": {\n \"phpunit/phpunit\": \"~4.8\"\n }\n}\n", "size": 269}', + '{"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}', + '{}', + '{"master": {"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}}' + ); + + $this->assertEquals( + array( + 'name' => 'user/repo', + 'description' => 'test repo', + 'license' => 'GPL', + 'authors' => array( + array( + 'name' => 'Name', + 'email' => 'local@domain.tld' + ) + ), + 'require' => array( + 'creator/package' => '^1.0' + ), + 'require-dev' => array( + 'phpunit/phpunit' => '~4.8' + ), + 'time' => '2016-05-17 13:19:52', + 'support' => array( + 'source' => 'https://bitbucket.org/user/repo/src/937992d19d72b5116c3e8c4a04f960e5fa270b22/?at=master' + ) + ), + $driver->getComposerInformation('master') + ); + } + + public function testGetTags() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + 'bitbucket.org', + 'https://api.bitbucket.org/1.0/repositories/user/repo/tags', + false + ) + ->willReturn( + '{"1.0.1": {"node": "9b78a3932143", "files": [{"type": "modified", "file": "path/to/file"}], "branches": [], "raw_author": "User ", "utctimestamp": "2015-04-16 14:50:40+00:00", "author": "user", "timestamp": "2015-04-16 16:50:40", "raw_node": "9b78a3932143497c519e49b8241083838c8ff8a1", "parents": ["84531c04dbfc", "50c2a4635ad0"], "branch": null, "message": "Commit message\n", "revision": null, "size": -1}, "1.0.0": {"node": "d3393d514318", "files": [{"type": "modified", "file": "path/to/file2"}], "branches": [], "raw_author": "User ", "utctimestamp": "2015-04-16 09:31:45+00:00", "author": "user", "timestamp": "2015-04-16 11:31:45", "raw_node": "d3393d514318a9267d2f8ebbf463a9aaa389f8eb", "parents": ["5a29a73cd1a0"], "branch": null, "message": "Commit message\n", "revision": null, "size": -1}}' + ); + + $this->assertEquals( + array( + '1.0.1' => '9b78a3932143497c519e49b8241083838c8ff8a1', + '1.0.0' => 'd3393d514318a9267d2f8ebbf463a9aaa389f8eb' + ), + $driver->getTags() + ); + } + + public function testGetBranches() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + 'bitbucket.org', + 'https://api.bitbucket.org/1.0/repositories/user/repo/branches', + false + ) + ->willReturn( + '{"master": {"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}}' + ); + + $this->assertEquals( + array( + 'master' => '937992d19d72b5116c3e8c4a04f960e5fa270b22' + ), + $driver->getBranches() + ); + } + + public function testSupports() + { + $this->assertTrue( + GitBitbucketDriver::supports($this->io, $this->config, 'https://bitbucket.org/user/repo.git') + ); + + $this->assertFalse( + GitBitbucketDriver::supports($this->io, $this->config, 'git@bitbucket.org:user/repo.git') + ); + + $this->assertFalse( + GitBitbucketDriver::supports($this->io, $this->config, 'https://github.com/user/repo.git') + ); + } +} diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index 5be6da7b0..9f6cd2a5f 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -21,30 +21,138 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase { private $username = 'username'; private $password = 'password'; - private $authcode = 'authcode'; + private $consumer_key = 'consumer_key'; + private $consumer_secret = 'consumer_secret'; private $message = 'mymessage'; private $origin = 'bitbucket.org'; private $token = 'bitbuckettoken'; + /** @type \Composer\IO\ConsoleIO|\PHPUnit_Framework_MockObject_MockObject */ + private $io; + /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ + private $rfs; + /** @type \Composer\Config|\PHPUnit_Framework_MockObject_MockObject */ + private $config; + /** @type Bitbucket */ + private $bitbucket; + + protected function setUp() + { + $this->io = $this + ->getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock() + ; + + $this->rfs = $this + ->getMockBuilder('Composer\Util\RemoteFilesystem') + ->disableOriginalConstructor() + ->getMock() + ; + + $this->config = $this->getMock('Composer\Config'); + + $this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->rfs); + } + + public function testRequestAccessTokenWithValidOAuthConsumer() + { + $this->io->expects($this->once()) + ->method('setAuthentication') + ->with($this->origin, $this->consumer_key, $this->consumer_secret); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + $this->origin, + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + false, + array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => 'grant_type=client_credentials', + ) + ) + ) + ->willReturn( + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', + $this->token + ) + ); + + $this->assertEquals( + array( + 'access_token' => $this->token, + 'scopes' => 'repository', + 'expires_in' => 3600, + 'refresh_token' => 'refreshtoken', + 'token_type' => 'bearer' + ), + $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) + ); + } + + public function testRequestAccessTokenWithUsernameAndPassword() + { + $this->io->expects($this->once()) + ->method('setAuthentication') + ->with($this->origin, $this->username, $this->password); + + $this->io->expects($this->any()) + ->method('writeError') + ->withConsecutive( + array('Invalid OAuth consumer provided.'), + array('This can have two reasons:'), + array('1. You are authenticating with a bitbucket username/password combination'), + array('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url') + ); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + $this->origin, + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + false, + array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => 'grant_type=client_credentials', + ) + ) + ) + ->willThrowException( + new \Composer\Downloader\TransportException( + sprintf( + 'The \'%s\' URL could not be accessed: HTTP/1.1 400 BAD REQUEST', + Bitbucket::OAUTH2_ACCESS_TOKEN_URL + ), + 400 + ) + ); + + $this->assertEquals(array(), $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); + } + public function testUsernamePasswordAuthenticationFlow() { - $io = $this->getIOMock(); - $io + $this->io ->expects($this->at(0)) ->method('writeError') ->with($this->message) ; - $io->expects($this->exactly(2)) + $this->io->expects($this->exactly(2)) ->method('askAndHideAnswer') ->withConsecutive( array('Consumer Key (hidden): '), array('Consumer Secret (hidden): ') ) - ->willReturnOnConsecutiveCalls($this->username, $this->password); + ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $this->rfs ->expects($this->once()) ->method('getContents') ->with( @@ -56,50 +164,18 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase ->willReturn(sprintf('{}', $this->token)) ; - $config = $this->getConfigMock(); - $config + $this->config ->expects($this->exactly(2)) ->method('getAuthConfigSource') ->willReturn($this->getAuthJsonMock()) ; - $config + $this->config ->expects($this->once()) ->method('getConfigSource') ->willReturn($this->getConfJsonMock()) ; - $bitbucket = new Bitbucket($io, $config, null, $rfs); - - $this->assertTrue($bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); - } - - private function getIOMock() - { - $io = $this - ->getMockBuilder('Composer\IO\ConsoleIO') - ->disableOriginalConstructor() - ->getMock() - ; - - return $io; - } - - private function getConfigMock() - { - $config = $this->getMock('Composer\Config'); - - return $config; - } - - private function getRemoteFilesystemMock() - { - $rfs = $this - ->getMockBuilder('Composer\Util\RemoteFilesystem') - ->disableOriginalConstructor() - ->getMock() - ; - - return $rfs; + $this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } private function getAuthJsonMock()