diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0d76554 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto + +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/CHANGELOG.md export-ignore +/README.md export-ignore diff --git a/.gitignore b/.gitignore index 82405c2..bfcf449 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ build vendor composer.lock -phpunit.xml \ No newline at end of file +phpunit.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..093a889 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: php + +php: + - 5.5 + - 5.6 + - 7.0 + - hhvm + +sudo: false + +matrix: + allow_failures: + - php: 7.0 + - php: hhvm + +install: + - composer install + +script: + - vendor/bin/phpunit --configuration build/phpunit.xml && cat build/testdox.txt build/coverage.txt + +after_script: + - php vendor/bin/coveralls -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 576590e..c386634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,42 @@ # Change Log All notable changes to the `flysystem-github` project will be documented in this -file. This project adheres to [Semantic Versioning](http://semver.org/). +file. This project adheres to the [keep-a-changelog](http://keepachangelog.com/) +and [Semantic Versioning](http://semver.org/) conventions. -## 0.1.0 - 2015-07-18 - Read functionality + + +## v0.2.0 - 2015-07-21 - Improvements and UnitTests + +### Added +- Adds automated checks (a.k.a. unit-tests) for the Adapter, Client and Settings classes. +- Adds various utility files for Travis builds, Coveralls and Composer + +### Changed +- Makes the PHPUnit configuration more strict +- Renames the Client class to "Api" + +## v0.1.0 - 2015-07-18 - Read functionality + +### Added +- Read functionality and Github API authentication have been implemented. + +## v0.0.0 - 2015-05-11 - Project Setup -## 0.0.0 - 2015-05-11 - Project Setup ### Added -Set up project basics like .gitignore file, PHPUnit Configuration file, +- Set up project basics like .gitignore file, PHPUnit Configuration file, Contributing guidelines, Composer file stating dependencies, MIT License, README file and this CHANGELOG file. -[unreleased]: https://github.com/potherca/flystystem-github/compare/v0.1.0...HEAD +[unreleased]: https://github.com/potherca/flystystem-github/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/potherca/flystystem-github/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/potherca/flystystem-github/compare/v0.0.0...v0.1.0 +[keep-a-changelog]: http://keepachangelog.com/ +[Semantic Versioning]: http://semver.org/ diff --git a/README.md b/README.md index bc81427..854f5c7 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,11 @@ [![Latest Version](https://img.shields.io/github/release/potherca/flysystem-github.svg?style=flat-square)](https://github.com/potherca/flysystem-github/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) -[![Build Status](https://img.shields.io/travis/potherca/flysystem-github/master.svg?style=flat-square)](https://travis-ci.org/potherca/flysystem-github) -[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/potherca/flysystem-github.svg?style=flat-square)](https://scrutinizer-ci.com/g/potherca/flysystem-github/code-structure) +[![Build Status](https://img.shields.io/travis/potherca/flysystem-github.svg?style=flat-square)](https://travis-ci.org/potherca/flysystem-github) +[![Coverage Status](https://coveralls.io/repos/potherca/flysystem-github/badge.svg)](https://coveralls.io/github/potherca/flysystem-github) [![Quality Score](https://img.shields.io/scrutinizer/g/potherca/flysystem-github.svg?style=flat-square)](https://scrutinizer-ci.com/g/potherca/flysystem-github) [![Total Downloads](https://img.shields.io/packagist/dt/potherca/flysystem-github.svg?style=flat-square)](https://packagist.org/packages/potherca/flysystem-github) - ## Install Via Composer @@ -28,9 +27,9 @@ limit. ### Basic Usage ```php -use Github\Client as GithubClient; +use Github\Client; use League\Flysystem\Filesystem; -use Potherca\Flysystem\Github\Client; +use Potherca\Flysystem\Github\Api; use Potherca\Flysystem\Github\GithubAdapter; use Potherca\Flysystem\Github\Settings; @@ -38,17 +37,17 @@ $project = 'thephpleague/flysystem'; $settings = new Settings($project); -$client = new Client(new GithubClient(), $settings); -$adapter = new GithubAdapter($client); +$api = new Api(new Client(), $settings); +$adapter = new GithubAdapter($api); $filesystem = new Filesystem($adapter); ``` ### Authentication ```php -use Github\Client as GithubClient; +use Github\Client; use League\Flysystem\Filesystem; -use Potherca\Flysystem\Github\Client; +use Potherca\Flysystem\Github\Api; use Potherca\Flysystem\Github\GithubAdapter; use Potherca\Flysystem\Github\Settings; @@ -58,19 +57,19 @@ $credentials = [Settings::AUTHENTICATE_USING_TOKEN, '83347e315b8bb4790a48ed6953a $settings = new Settings($project, $credentials); -$client = new Client(new GithubClient(), $settings); -$adapter = new GithubAdapter($client); +$api = new Api(new Client(), $settings); +$adapter = new GithubAdapter($api); $filesystem = new Filesystem($adapter); ``` ### Cache Usage ```php -use Github\Client as GithubClient; +use Github\Client; use Github\HttpClient\CachedHttpClient as CachedClient; use Github\HttpClient\Cache\FilesystemCache as Cache; use League\Flysystem\Filesystem; -use Potherca\Flysystem\Github\Client; +use Potherca\Flysystem\Github\Api; use Potherca\Flysystem\Github\GithubAdapter; use Potherca\Flysystem\Github\Settings; @@ -82,8 +81,8 @@ $cache = new Cache('/tmp/github-api-cache') $cacheClient = new CachedClient(); $cacheClient->setCache($cache); -$client = new Client($cacheClient, $settings); -$adapter = new GithubAdapter($client); +$api = new Api($cacheClient, $settings); +$adapter = new GithubAdapter($api); $filesystem = new Filesystem($adapter); ``` @@ -94,13 +93,17 @@ $filesystem = new Filesystem($adapter); $ composer test ``` +## Security + +If you discover any security related issues, please email potherca@gmail.com instead of using the issue tracker. + ## Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) for details. -## Security +## Change Log -If you discover any security related issues, please email potherca@gmail.com instead of using the issue tracker. +Please see [CHANGELOG](CHANGELOG.md) for details. ## Credits diff --git a/build/phpunit.xml b/build/phpunit.xml new file mode 100644 index 0000000..df9f636 --- /dev/null +++ b/build/phpunit.xml @@ -0,0 +1,35 @@ + + + + + ../tests + + + + + src/ + + + + + + + + + + diff --git a/composer.json b/composer.json index 8e0aa82..bc1dfeb 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,15 @@ } ], "require": { - "php" : ">=5.3.0", + "php" : ">=5.5", "knplabs/github-api": "^1.4", "league/flysystem": "^1.0" }, "require-dev": { - "phpunit/phpunit" : "4.*", - "scrutinizer/ocular": "~1.1" + "phpunit/phpunit" : "^4.7.7", + "satooshi/php-coveralls": "^0.6.1", + "scrutinizer/ocular": "^1.1", + "whatthejeff/nyancat-phpunit-resultprinter": "^1.2" }, "autoload": { "psr-4": { @@ -40,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "0.2.0-dev" } }, "scripts": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 68bbdfd..9b8dda9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,25 @@ - + - + tests @@ -20,10 +29,9 @@ - - - - - + + + + diff --git a/src/Api.php b/src/Api.php new file mode 100644 index 0000000..c5fbbef --- /dev/null +++ b/src/Api.php @@ -0,0 +1,398 @@ +authenticate(); + return $this->client->api($name); + } + + /** + * @return GitData + */ + private function getGitDataApi() + { + return $this->getApi(self::API_GIT_DATA); + } + + /** + * @return Repo + */ + private function getRepositoryApi() + { + return $this->getApi(self::API_REPO); + } + + /** + * @return \Github\Api\Repository\Contents + */ + private function getRepositoryContent() + { + return $this->getRepositoryApi()->contents(); + } + + //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + final public function __construct(Client $client, SettingsInterface $settings) + { + /* @NOTE: If $settings contains `credentials` but not an `author` we are + * still in `read-only` mode. + */ + + $this->client = $client; + $this->settings = $settings; + } + + /** + * @param string $path + * + * @return bool + */ + final public function exists($path) + { + return $this->getRepositoryContent()->exists( + $this->settings->getVendor(), + $this->settings->getPackage(), + $path, + $this->settings->getReference() + ); + } + + /** + * @param $path + * + * @return null|string + * + * @throws \Github\Exception\ErrorException + */ + final public function getFileContents($path) + { + return $this->getRepositoryContent()->download( + $this->settings->getVendor(), + $this->settings->getPackage(), + $path, + $this->settings->getReference() + ); + } + + /** + * @param string $path + * + * @return array + */ + final public function getLastUpdatedTimestamp($path) + { + $commits = $this->commitsForFile($path); + + $updated = array_shift($commits); + + $time = new \DateTime($updated['commit']['committer']['date']); + + return ['timestamp' => $time->getTimestamp()]; + } + + /** + * @param string $path + * + * @return array + */ + final public function getCreatedTimestamp($path) + { + $commits = $this->commitsForFile($path); + + $created = array_pop($commits); + + $time = new \DateTime($created['commit']['committer']['date']); + + return ['timestamp' => $time->getTimestamp()]; + } + + /** + * @param string $path + * + * @return array|bool + */ + final public function getMetaData($path) + { + try { + $metadata = $this->getRepositoryContent()->show( + $this->settings->getVendor(), + $this->settings->getPackage(), + $path, + $this->settings->getReference() + ); + } catch (RuntimeException $exception) { + if ($exception->getMessage() === self::ERROR_NOT_FOUND) { + $metadata = false; + } else { + throw $exception; + } + } + + return $metadata; + } + + /** + * @param string $path + * @param bool $recursive + * + * @return array + */ + final public function getRecursiveMetadata($path, $recursive) + { + // If $info['truncated'] is `true`, the number of items in the tree array + // exceeded the github maximum limit. If you need to fetch more items, + // multiple calls will be needed + + $info = $this->getGitDataApi()->trees()->show( + $this->settings->getVendor(), + $this->settings->getPackage(), + $this->settings->getReference(), + $recursive + ); + + $treeMetadata = $this->extractMetaDataFromTreeInfo($info[self::KEY_TREE], $path, $recursive); + + return $this->normalizeTreeMetadata($treeMetadata); + } + + /** + * @param string $path + * + * @return null|string + */ + final public function guessMimeType($path) + { + //@NOTE: The github API does not return a MIME type, so we have to guess :-( + if (strrpos($path, '.') > 1) { + $extension = substr($path, strrpos($path, '.')+1); + $mimeType = MimeType::detectByFileExtension($extension) ?: 'text/plain'; + } else { + $content = $this->getFileContents($path); + $mimeType = MimeType::detectByContent($content); + } + + return $mimeType; + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * + */ + private function authenticate() + { + if ($this->isAuthenticationAttempted === false) { + $credentials = $this->settings->getCredentials(); + + if (empty($credentials) === false) { + $credentials = array_replace( + [null, null, null], + $credentials + ); + + $this->client->authenticate( + $credentials[1], + $credentials[2], + $credentials[0] + ); + } + $this->isAuthenticationAttempted = true; + } + } + + /** + * @param array $tree + * @param string $path + * @param bool $recursive + * + * @return array + */ + private function extractMetaDataFromTreeInfo(array $tree, $path, $recursive) + { + if(empty($path) === false) { + $metadata = array_filter($tree, function ($entry) use ($path, $recursive) { + $match = false; + + if (strpos($entry[self::KEY_PATH], $path) === 0) { + if ($recursive === true) { + $match = true; + } else { + $length = strlen($path); + $match = (strpos($entry[self::KEY_PATH], '/', $length) === false); + } + } + + return $match; + }); + } elseif ($recursive === false) { + $metadata = array_filter($tree, function ($entry) use ($path) { + return (strpos($entry[self::KEY_PATH], '/', strlen($path)) === false); + }); + } else { + $metadata = $tree; + } + + return $metadata; + } + + /** + * @param $permissions + * @return string + */ + private function guessVisibility($permissions) + { + return $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; + } + + /** + * @param array $metadata + * + * @return array + */ + private function normalizeTreeMetadata($metadata) + { + $result = []; + + if (is_array(current($metadata)) === false) { + $metadata = [$metadata]; + } + + foreach ($metadata as $entry) { + $this->setEntryName($entry); + $this->setEntryType($entry); + $this->setEntryVisibility($entry); + + $this->setDefaultValue($entry, self::KEY_CONTENTS); + $this->setDefaultValue($entry, self::KEY_STREAM); + $this->setDefaultValue($entry, self::KEY_TIMESTAMP); + + + $result[] = $entry; + } + + return $result; + } + + /** + * @param $path + * + * @return array + */ + private function commitsForFile($path) + { + return $this->getRepositoryApi()->commits()->all( + $this->settings->getVendor(), + $this->settings->getPackage(), + array( + 'sha' => $this->settings->getBranch(), + 'path' => $path + ) + ); + } + + /** + * @param array $entry + * @param string $key + * @param bool $default + * + * @return mixed + */ + private function setDefaultValue(array &$entry, $key, $default = false) + { + if (isset($entry[$key]) === false) { + $entry[$key] = $default; + } + } + + /** + * @param $entry + */ + private function setEntryType(&$entry) + { + if (isset($entry[self::KEY_TYPE]) === true) { + switch ($entry[self::KEY_TYPE]) { + case self::KEY_BLOB: + $entry[self::KEY_TYPE] = self::KEY_FILE; + break; + + case self::KEY_TREE: + $entry[self::KEY_TYPE] = self::KEY_DIRECTORY; + break; + } + } + } + + /** + * @param $entry + */ + private function setEntryVisibility(&$entry) + { + if (isset($entry[self::KEY_MODE])) { + $entry[self::KEY_VISIBILITY] = $this->guessVisibility($entry[self::KEY_MODE]); + } else { + $entry[self::KEY_VISIBILITY] = false; + } + } + + /** + * @param $entry + */ + private function setEntryName(&$entry) + { + if (isset($entry[self::KEY_NAME]) === false) { + if (isset($entry[self::KEY_FILENAME]) === true) { + $entry[self::KEY_NAME] = $entry[self::KEY_FILENAME]; + } elseif (isset($entry[self::KEY_PATH]) === true) { + $entry[self::KEY_NAME] = $entry[self::KEY_PATH]; + } else { + $entry[self::KEY_NAME] = null; + } + } + } +} diff --git a/src/ApiInterface.php b/src/ApiInterface.php new file mode 100644 index 0000000..d8cb101 --- /dev/null +++ b/src/ApiInterface.php @@ -0,0 +1,53 @@ +client = $client; - $this->settings = $settings; - - /* @NOTE: If $settings contains `credentials` but not an `author` we are - * still in `read-only` mode. - */ - list($this->vendor, $this->package) = explode('/', $this->settings->getRepository()); - } - - /** - * @param string $path - * - * @return bool - */ - final public function exists($path) - { - return $this->repositoryContents()->exists( - $this->vendor, - $this->package, - $path, - $this->settings->getReference() - ); - } - - /** - * @param $path - * - * @return null|string - * - * @throws \Github\Exception\ErrorException - */ - final public function download($path) - { - $fileContent = $this->repositoryContents()->download( - $this->vendor, - $this->package, - $path, - $this->settings->getReference() - ); - - return $fileContent; - } - - /** - * @param string $path - * @param bool $recursive - * - * @return array - */ - final public function metadata($path, $recursive) - { - // If $info['truncated'] is `true`, the number of items in the tree array - // exceeded the github maximum limit. If you need to fetch more items, - // multiple calls will be needed - - $info = $this->trees($recursive); - $tree = $this->getPathFromTree($info, $path, $recursive); - $result = $this->normalizeMetadata($tree); - - return $result; - } - - /** - * @param string $path - * - * @return array - */ - final public function show($path) - { - // Get information about a repository file or directory - $fileInfo = $this->repositoryContents()->show( - $this->vendor, - $this->package, - $path, - $this->settings->getReference() - ); - return $fileInfo; - } - - /** - * @param string $path - * - * @return array|bool - */ - final public function getMetaData($path) - { - try { - $metadata = $this->show($path); - } catch (RuntimeException $exception) { - if ($exception->getMessage() === self::ERROR_NOT_FOUND) { - $metadata = false; - } else { - throw $exception; - } - } - - return $metadata; - } - - /** - * @param string $path - * - * @return null|string - */ - final public function guessMimeType($path) - { - //@NOTE: The github API does not return a MIME type, so we have to guess :-( - if (strrpos($path, '.') > 1) { - $extension = substr($path, strrpos($path, '.')+1); - } - - if (isset($extension)) { - $mimeType = MimeType::detectByFileExtension($extension) ?: 'text/plain'; - } else { - $content = $this->download($path); - $mimeType = MimeType::detectByContent($content); - } - - return $mimeType; - } - - /** - * @param string $path - * - * @return array - */ - final public function updated($path) - { - // List commits for a file - $commits = $this->repository()->commits()->all( - $this->vendor, - $this->package, - array( - 'sha' => $this->settings->getBranch(), - 'path' => $path - ) - ); - - $updated = array_shift($commits); - //@NOTE: $created = array_pop($commits); - - $time = new \DateTime($updated['commit']['committer']['date']); - - return ['timestamp' => $time->getTimestamp()]; - } - - /** - * @return \Github\Api\Repository\Contents - */ - private function repositoryContents() - { - return $this->repository()->contents(); - } - - /** - * - */ - private function authenticate() - { - static $hasRun; - - if ($hasRun === null) { - if (empty($this->settings->getCredentials()) === false) { - $credentials = array_replace( - [null, null, null], - $this->settings->getCredentials() - ); - - $this->client->authenticate( - $credentials[1], - $credentials[2], - $credentials[0] - ); - } - $hasRun = true; - } - } - - /** - * @return Repo - */ - private function repository() - { - return $this->fetchApi(self::KEY_REPO); - } - - /** - * @param string $name - * @return \Github\Api\ApiInterface - */ - private function fetchApi($name) - { - $this->authenticate(); - return $this->client->api($name); - } - - /** - * @param array $metadata - * @param string $path - * @param bool $recursive - * - * @return array - */ - private function getPathFromTree(array $metadata, $path, $recursive) - { - if (empty($path)) { - if ($recursive === false) { - $metadata = array_filter($metadata, function ($entry) use ($path) { - return (strpos($entry[self::KEY_PATH], '/', strlen($path)) === false); - }); - } - } else { - $metadata = array_filter($metadata, function ($entry) use ($path, $recursive) { - $match = false; - - if (strpos($entry[self::KEY_PATH], $path) === 0) { - if ($recursive === true) { - $match = true; - } else { - $length = strlen($path); - $match = (strpos($entry[self::KEY_PATH], '/', $length) === false); - } - } - - return $match; - }); - } - - return $metadata; - } - - /** - * @param array $metadata - * - * @return array - */ - private function normalizeMetadata($metadata) - { - $result = []; - - if (is_array(current($metadata)) === false) { - $metadata = [$metadata]; - } - - foreach ($metadata as $entry) { - if (isset($entry[self::KEY_NAME]) === false){ - if(isset($entry[self::KEY_FILENAME]) === true) { - $entry[self::KEY_NAME] = $entry[self::KEY_FILENAME]; - } elseif(isset($entry[self::KEY_PATH]) === true) { - $entry[self::KEY_NAME] = $entry[self::KEY_PATH]; - } else { - // ? - } - } - - if (isset($entry[self::KEY_TYPE]) === true) { - switch ($entry[self::KEY_TYPE]) { - case self::KEY_BLOB: - $entry[self::KEY_TYPE] = self::KEY_FILE; - break; - - case self::KEY_TREE: - $entry[self::KEY_TYPE] = self::KEY_DIRECTORY; - break; - } - } - - if (isset($entry[self::KEY_CONTENTS]) === false) { - $entry[self::KEY_CONTENTS] = false; - } - - if (isset($entry[self::KEY_STREAM]) === false) { - $entry[self::KEY_STREAM] = false; - } - - if (isset($entry[self::KEY_TIMESTAMP]) === false) { - $entry[self::KEY_TIMESTAMP] = false; - } - - if (isset($entry[self::KEY_MODE])) { - $entry[self::KEY_VISIBILITY] = $this->visibility($entry[self::KEY_MODE]); - } else { - $entry[self::KEY_VISIBILITY] = false; - } - - $result[] = $entry; - } - - return $result; - } - - /** - * @return GitData - */ - private function gitData() - { - return $this->fetchApi(self::KEY_GIT_DATA); - } - - /** - * @param bool $recursive - * @return \Guzzle\Http\EntityBodyInterface|mixed|string - */ - private function trees($recursive) - { - $trees = $this->gitData()->trees(); - - $info = $trees->show( - $this->vendor, - $this->package, - $this->settings->getReference(), - $recursive - ); - - return $info[self::KEY_TREE]; - } - - /** - * @param $permissions - * @return string - */ - private function visibility($permissions) - { - return $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; - } -} diff --git a/src/GithubAdapter.php b/src/GithubAdapter.php index c4e5d2a..f48345f 100644 --- a/src/GithubAdapter.php +++ b/src/GithubAdapter.php @@ -21,15 +21,23 @@ class GithubAdapter extends AbstractAdapter const VISIBILITY_PRIVATE = 'private'; const VISIBILITY_PUBLIC = 'public'; - /** @var Client */ - private $client; + /** @var ApiInterface */ + private $api; /** - * @param Client $client + * @return ApiInterface */ - public function __construct(Client $client) + final public function getApi() { - $this->client = $client; + return $this->api; + } + + /** + * @param ApiInterface $api + */ + public function __construct(ApiInterface $api) + { + $this->api = $api; } /** @@ -44,7 +52,7 @@ public function __construct(Client $client) public function write($path, $contents, Config $config) { throw new Exception('Write action are not (yet) supported'); - //@TODO: return $this->client->create($path, $contents); + //@TODO: return $this->getApi()->create($path, $contents); } /** @@ -59,7 +67,7 @@ public function write($path, $contents, Config $config) public function update($path, $contents, Config $config) { throw new Exception('Write action are not (yet) supported'); - // @TODO: return $this->client->update($path, $contents); + // @TODO: return $this->getApi()->update($path, $contents); } /** @@ -73,7 +81,7 @@ public function update($path, $contents, Config $config) public function rename($path, $newpath) { throw new Exception('Write action are not (yet) supported'); - // @TODO: return $this->client->rename($path, $newPath); + // @TODO: return $this->getApi()->rename($path, $newPath); } /** @@ -87,7 +95,7 @@ public function rename($path, $newpath) public function copy($path, $newpath) { throw new Exception('Write action are not (yet) supported'); - // @TODO: return $this->client->copy($path, $newPath); + // @TODO: return $this->getApi()->copy($path, $newPath); } /** @@ -100,7 +108,7 @@ public function copy($path, $newpath) public function delete($path) { throw new Exception('Write action are not (yet) supported'); - // @TODO: return $this->client->delete($path); + // @TODO: return $this->getApi()->delete($path); } /** @@ -113,7 +121,7 @@ public function delete($path) public function deleteDir($dirname) { throw new Exception('Write action are not (yet) supported'); - // @TODO: return $this->client->deleteDir($dirname); + // @TODO: return $this->getApi()->deleteDir($dirname); } /** @@ -127,7 +135,7 @@ public function deleteDir($dirname) public function createDir($dirname, Config $config) { throw new Exception('Write action are not (yet) supported'); - // @TODO: return $this->client->createDir($dirname); + // @TODO: return $this->getApi()->createDir($dirname); } /** @@ -152,7 +160,7 @@ public function setVisibility($path, $visibility) */ public function has($path) { - return $this->client->exists($path); + return $this->getApi()->exists($path); } /** @@ -164,7 +172,7 @@ public function has($path) */ public function read($path) { - return [Client::KEY_CONTENTS => $this->client->download($path)]; + return [ApiInterface::KEY_CONTENTS => $this->getApi()->getFileContents($path)]; } /** @@ -177,7 +185,7 @@ public function read($path) */ public function listContents($path = '/', $recursive = false) { - return $this->client->metadata($path, $recursive); + return $this->getApi()->getRecursiveMetadata($path, $recursive); } /** @@ -189,7 +197,7 @@ public function listContents($path = '/', $recursive = false) */ public function getMetadata($path) { - return $this->client->show($path); + return $this->getApi()->getMetaData($path); } /** @@ -201,7 +209,7 @@ public function getMetadata($path) */ public function getSize($path) { - return $this->client->getMetaData($path); + return $this->getApi()->getMetaData($path); } /** @@ -213,7 +221,7 @@ public function getSize($path) */ public function getMimetype($path) { - return ['mimetype' => $this->client->guessMimeType($path)]; + return ['mimetype' => $this->getApi()->guessMimeType($path)]; } /** @@ -225,7 +233,7 @@ public function getMimetype($path) */ public function getTimestamp($path) { - return $this->client->updated($path); + return $this->getApi()->getLastUpdatedTimestamp($path); } /** @@ -238,7 +246,7 @@ public function getTimestamp($path) public function getVisibility($path) { $recursive = false; - $metadata = $this->client->metadata($path, $recursive); + $metadata = $this->getApi()->getRecursiveMetadata($path, $recursive); return $metadata[0]; } } diff --git a/src/Settings.php b/src/Settings.php index 706eb28..8ea14b1 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -4,42 +4,53 @@ use Github\Client; -class Settings +class Settings implements SettingsInterface { + ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ const AUTHENTICATE_USING_TOKEN = Client::AUTH_URL_TOKEN; const AUTHENTICATE_USING_PASSWORD = Client::AUTH_HTTP_PASSWORD; - const REFERENCE_HEAD = 'HEAD'; const BRANCH_MASTER = 'master'; + const REFERENCE_HEAD = 'HEAD'; + + const ERROR_INVALID_REPOSITORY_NAME = 'Given Repository name "%s" should be in the format of "vendor/project"'; /** @var string */ - private $repository; - /** @var string */ - private $reference = self::REFERENCE_HEAD; + private $branch; /** @var array */ private $credentials; /** @var string */ - private $branch = self::BRANCH_MASTER; + private $reference; + /** @var string */ + private $repository; + /** @var string */ + private $vendor; + /** @var string */ + private $package; - final public function __construct( - $repository, - array $credentials = [], - $branch = self::BRANCH_MASTER, - $reference = self::REFERENCE_HEAD - ) { - $this->branch = $branch; - $this->credentials = $credentials; - $this->reference = $reference; - $this->repository = $repository; + //////////////////////////// SETTERS AND GETTERS \\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @return string + */ + final public function getBranch() + { + return $this->branch; } + /** + * @return array + */ + final public function getCredentials() + { + return $this->credentials; + } /** * @return string */ - final public function getRepository() + final public function getPackage() { - return $this->repository; + return $this->package; } /** @@ -51,19 +62,55 @@ final public function getReference() } /** - * @return array + * @return string */ - final public function getCredentials() + final public function getRepository() { - return $this->credentials; + return $this->repository; } /** * @return string */ - final public function getBranch() + final public function getVendor() { - return $this->branch; + return $this->vendor; + } + + //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + final public function __construct( + $repository, + array $credentials = [], + $branch = self::BRANCH_MASTER, + $reference = self::REFERENCE_HEAD + ) { + $this->isValidRepositoryName($repository); + + $this->branch = (string) $branch; + $this->credentials = $credentials; + $this->reference = (string) $reference; + $this->repository = (string) $repository; + + list($this->vendor, $this->package) = explode('/', $repository); + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @param $repository + */ + private function isValidRepositoryName($repository) + { + if (is_string($repository) === false + || strpos($repository, '/') === false + || strpos($repository, '/') === 0 + || substr_count($repository, '/') !== 1 + ) { + $message = sprintf( + self::ERROR_INVALID_REPOSITORY_NAME, + var_export($repository, true) + ); + throw new \InvalidArgumentException($message); + } } } diff --git a/src/SettingsInterface.php b/src/SettingsInterface.php new file mode 100644 index 0000000..92518bf --- /dev/null +++ b/src/SettingsInterface.php @@ -0,0 +1,36 @@ + + * @covers ::__construct + */ +class ApiTest extends \PHPUnit_Framework_TestCase +{ + ////////////////////////////////// FIXTURES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + const MOCK_FILE_PATH = '/path/to/mock/file'; + const MOCK_FILE_CONTENTS = 'Mock file contents'; + + /** @var Api */ + private $api; + /** @var Client|\PHPUnit_Framework_MockObject_MockObject */ + private $mockClient; + /** @var Settings|\PHPUnit_Framework_MockObject_MockObject */ + private $mockSettings; + + /** + * + */ + protected function setUp() + { + $this->mockClient = $this->getMockClient(); + $this->mockSettings = $this->getMockSettings(); + + $this->api = new Api($this->mockClient, $this->mockSettings); + } + + /////////////////////////////////// TESTS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @uses Potherca\Flysystem\Github\Api::exists + */ + final public function testApiShouldComplainWhenInstantiatedWithoutClient() + { + $message = sprintf( + 'Argument %d passed to %s::__construct() must be an instance of %s', + 1, + Api::class, + Client::class + ); + + $this->setExpectedException( + \PHPUnit_Framework_Error::class, + $message + ); + + /** @noinspection PhpParamsInspection */ + new Api(); + } + + /** + * @coversNothing + */ + final public function testApiShouldComplainWhenInstantiatedWithoutSettings() + { + $message = sprintf( + 'Argument %d passed to %s::__construct() must implement interface %s', + 2, + Api::class, + SettingsInterface::class + ); + + $this->setExpectedException( + \PHPUnit_Framework_Error::class, + $message + ); + + /** @noinspection PhpParamsInspection */ + new Api($this->getMockClient()); + } + + /** + * @covers ::getFileContents + */ + final public function testApiShouldUseValuesFromSettingsWhenAskingClientForFileContent() + { + $api = $this->api; + + $expected = self::MOCK_FILE_CONTENTS; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $this->prepareMockApi( + 'download', + $api::API_REPO, + [$mockVendor, $mockPackage, self::MOCK_FILE_PATH, $mockReference], + $expected + ); + + $actual = $api->getFileContents(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::exists + */ + final public function testApiShouldUseValuesFromSettingsWhenAskingClientIfFileExists() + { + $api = $this->api; + + $expected = self::MOCK_FILE_CONTENTS; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $this->prepareMockApi( + 'exists', + $api::API_REPO, + [$mockVendor, $mockPackage, self::MOCK_FILE_PATH, $mockReference], + $expected + ); + + $actual = $api->exists(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getLastUpdatedTimestamp + */ + final public function testApiShouldUseValuesFromSettingsWhenAskingClientForLastUpdatedTimestamp() + { + $api = $this->api; + + $expected = ['timestamp' => 1420070400]; + + $this->prepareFixturesForTimeStamp(); + + $actual = $api->getLastUpdatedTimestamp(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getCreatedTimestamp + */ + final public function testApiShouldUseValuesFromSettingsWhenAskingClientForCreatedTimestamp() + { + $api = $this->api; + + $expected = ['timestamp' => 1362268800]; + + $this->prepareFixturesForTimeStamp(); + + $actual = $api->getCreatedTimestamp(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + /** + * @covers ::getMetaData + */ + final public function testApiShouldUseValuesFromSettingsWhenAskingClientForFileInfo() + { + $api = $this->api; + + $expected = self::MOCK_FILE_CONTENTS; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $this->prepareMockApi( + 'show', + $api::API_REPO, + [$mockVendor, $mockPackage, self::MOCK_FILE_PATH, $mockReference], + $expected + ); + + $actual = $api->getMetaData(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getMetaData + */ + final public function testApiShouldAccountForFileNotExistingWhenAskingInfoForFile() + { + $api = $this->api; + + $expected = false; + + $this->mockClient->expects($this->exactly(1)) + ->method('api') + ->willThrowException(new RuntimeException(Api::ERROR_NOT_FOUND)); + + $actual = $api->getMetaData(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getMetaData + */ + final public function testApiShouldPassOtherRuntimeExceptionsWhenAskingInfoForFileCausesRuntimeException() + { + $api = $this->api; + + $this->setExpectedException(RuntimeException::class, self::MOCK_FILE_CONTENTS); + + $expected = false; + + $this->mockClient->expects($this->exactly(1)) + ->method('api') + ->willThrowException(new RuntimeException(self::MOCK_FILE_CONTENTS)); + + $actual = $api->getMetaData(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getMetaData + */ + final public function testApiShouldPassOnExceptionsWhenAskingInfoForFileCausesAnException() + { + $api = $this->api; + + $this->setExpectedException(\RuntimeException::class, Api::ERROR_NOT_FOUND); + + $expected = false; + + $this->mockClient->expects($this->exactly(1)) + ->method('api') + ->willThrowException(new \RuntimeException(Api::ERROR_NOT_FOUND)); + + $actual = $api->getMetaData(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getRecursiveMetadata + * + * @dataProvider provideExpectedMetadata + * + * @param string $path + * @param array $expected + * @param bool $recursive + * @param bool $truncated + */ + final public function testApiShouldRetrieveExpectedMetadataWhenAskedTogetRecursiveMetadata( + $path, + $expected, + $recursive, + $truncated + ) { + $api = $this->api; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $this->prepareMockApi( + 'show', + $api::API_GIT_DATA, + [$mockVendor, $mockPackage, $mockReference, $recursive], + $this->getMockApiTreeResponse($truncated, $api), + Trees::class + ); + + $actual = $api->getRecursiveMetadata($path, $recursive); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::guessMimeType + * + * @uses League\Flysystem\Util\MimeType + */ + final public function testApiShouldUseFileExtensionToGuessMimeTypeWhenExtensionIsAvailable() + { + $api = $this->api; + + $expected = 'image/png'; + + $this->mockClient->expects($this->never())->method('api'); + + $actual = $api->guessMimeType(self::MOCK_FILE_PATH.'.png'); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::guessMimeType + * + * @uses League\Flysystem\Util\MimeType + * + * @uses Potherca\Flysystem\Github\Api::getFileContents + */ + final public function testApiShouldUseFileContentsToGuessMimeTypeWhenExtensionUnavailable() + { + $api = $this->api; + + $expected = 'image/png'; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $image = imagecreatetruecolor(1,1); + ob_start(); + imagepng($image); + $contents = ob_get_contents(); + ob_end_clean(); + imagedestroy($image); + + $this->prepareMockApi( + 'download', + $api::API_REPO, + [$mockVendor, $mockPackage, self::MOCK_FILE_PATH, $mockReference], + $contents + ); + + $actual = $api->guessMimeType(self::MOCK_FILE_PATH); + + $this->assertEquals($expected, $actual); + } + + /** + * @uses Potherca\Flysystem\Github\Api::exists + */ + final public function testApiShouldUseCredentialsWhenTheyHaveBeenGiven() + { + $api = $this->api; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + 'getCredentials' => ['foo'] + ]); + + $this->prepareMockApi( + 'exists', + $api::API_REPO, + [$mockVendor, $mockPackage, self::MOCK_FILE_PATH, $mockReference], + '' + ); + + $this->mockClient->expects($this->exactly(1)) + ->method('authenticate') + ; + + $api->exists(self::MOCK_FILE_PATH); + } + + ////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @return Client|\PHPUnit_Framework_MockObject_MockObject + */ + private function getMockClient() + { + return $this->getMockBuilder(Client::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return Settings|\PHPUnit_Framework_MockObject_MockObject + */ + private function getMockSettings() + { + return $this->getMockBuilder(SettingsInterface::class) + ->getMock(); + } + + /** + * @param string $method + * @param string $apiName + * @param array $apiParameters + * @param mixed $apiOutput + * @param string $repositoryClass + */ + private function prepareMockApi($method, $apiName, $apiParameters, $apiOutput, $repositoryClass = Contents::class) + { + + $parts = explode('\\', $repositoryClass); + $repositoryName = strtolower(array_pop($parts)); + + $mockApi = $this->getMockBuilder(ApiInterface::class) + ->setMethods([$repositoryName, 'getPerPage', 'setPerPage']) + ->getMock() + ; + + $mockRepository = $this->getMockBuilder($repositoryClass) + ->disableOriginalConstructor() + ->getMock() + ; + + $mockRepository->expects($this->exactly(1)) + ->method($method) + ->withAnyParameters() + ->willReturnCallback(function () use ($apiParameters, $apiOutput) { + $this->assertEquals($apiParameters, func_get_args()); + return $apiOutput; + }) + ; + + $mockApi->expects($this->exactly(1)) + ->method($repositoryName) + ->willReturn($mockRepository) + ; + + $this->mockClient->expects($this->exactly(1)) + ->method('api') + ->with($apiName) + ->willReturn($mockApi) + ; + } + + /** + * @param array $expectations + */ + private function prepareMockSettings(array $expectations) + { + foreach ($expectations as $methodName => $returnValue) { + $this->mockSettings->expects($this->exactly(1)) + ->method($methodName) + ->willReturn($returnValue) + ; + } + } + + /** + * @param $truncated + * @param $api + * @return array + */ + private function getMockApiTreeResponse($truncated, $api) + { + return [ + $api::KEY_TREE => [ + [ + 'path' => self::MOCK_FILE_PATH, + 'mode' => '100644', + 'type' => 'tree', + 'size' => 57, + ], + [ + 'path' => self::MOCK_FILE_PATH . 'Foo', + 'basename' => self::MOCK_FILE_PATH . 'Foo', + 'mode' => '100644', + 'type' => 'blob', + 'size' => 57, + ], + [ + 'path' => self::MOCK_FILE_PATH . '/Bar', + 'name' => self::MOCK_FILE_PATH . '/Bar', + 'mode' => '100644', + 'type' => 'blob', + 'size' => 57, + ], + [ + 'path' => 'some/other/file', + 'mode' => '100644', + 'type' => 'blob', + 'size' => 747, + ], + ], + 'truncated' => $truncated, + ]; + } + + private function prepareFixturesForTimeStamp() + { + date_default_timezone_set('UTC'); + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockBranch = 'branch'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getBranch' => $mockBranch, + ]); + + $apiParameters = [ + $mockVendor, + $mockPackage, + [ + 'sha' => $mockBranch, + 'path' => self::MOCK_FILE_PATH + ] + + ]; + + $apiOutput = [ + ['commit' => ['committer' => ['date' => '20150101']]], + ['commit' => ['committer' => ['date' => '20140202']]], + ['commit' => ['committer' => ['date' => '20130303']]], + ]; + + $this->prepareMockApi( + 'all', + Api::API_REPO, + $apiParameters, + $apiOutput, + Commits::class + ); + } + + /////////////////////////////// DATAPROVIDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @return array + */ + final public function provideExpectedMetadata() + { + return [ + 'Filepath, not recursive, not truncated' => [ + self::MOCK_FILE_PATH, + [ + [ + 'path' => '/path/to/mock/file', + 'mode' => 100644, + 'type' => 'dir', + 'size' => 57, + 'name' => '/path/to/mock/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/fileFoo', + 'basename' => '/path/to/mock/fileFoo', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/fileFoo', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ] + ], + false, + false + ], + 'Filepath, recursive, not truncated' => [ + self::MOCK_FILE_PATH, + [ + [ + 'path' => '/path/to/mock/file', + 'mode' => 100644, + 'type' => 'dir', + 'size' => 57, + 'name' => '/path/to/mock/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/fileFoo', + 'basename' => '/path/to/mock/fileFoo', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/fileFoo', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/file/Bar', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/file/Bar', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ] + ], + true, + false + ], + 'Filepath, not recursive, truncated' => [ + self::MOCK_FILE_PATH, + [ + [ + 'path' => '/path/to/mock/file', + 'mode' => 100644, + 'type' => 'dir', + 'size' => 57, + 'name' => '/path/to/mock/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/fileFoo', + 'basename' => '/path/to/mock/fileFoo', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/fileFoo', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ] + ], + false, + true + ], + 'No Filepath, recursive, not truncated' => [ + '', + [ + [ + 'path' => '/path/to/mock/file', + 'mode' => 100644, + 'type' => 'dir', + 'size' => 57, + 'name' => '/path/to/mock/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/fileFoo', + 'basename' => '/path/to/mock/fileFoo', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/fileFoo', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/file/Bar', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/file/Bar', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => 'some/other/file', + 'mode' => 100644, + 'type' => 'file', + 'size' => 747, + 'name' => 'some/other/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ] + ], + true, + false + ], + 'No Filepath, recursive, truncated' => [ + '', + [ + [ + 'path' => '/path/to/mock/file', + 'mode' => 100644, + 'type' => 'dir', + 'size' => 57, + 'name' => '/path/to/mock/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/fileFoo', + 'basename' => '/path/to/mock/fileFoo', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/fileFoo', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/file/Bar', + 'mode' => 100644, + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/file/Bar', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => 'some/other/file', + 'mode' => 100644, + 'type' => 'file', + 'size' => 747, + 'name' => 'some/other/file', + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + 'visibility' => 'public' + ] + ], + true, + true + ], + 'No Filepath, not recursive, truncated' => [ + '', + [ + [ + 'name' => null, + 'visibility' => null, + 'contents' => null, + 'stream' => null, + 'timestamp' => null + ] + ], + false, + true + ], + 'No Filepath, not recursive, not truncated' => [ + '', + [ + [ + 'name' => null, + 'visibility' => null, + 'contents' => null, + 'stream' => null, + 'timestamp' => null, + ] + ], + false, + false + ], + ]; + } +} diff --git a/tests/GithubAdapterTest.php b/tests/GithubAdapterTest.php new file mode 100644 index 0000000..54c10af --- /dev/null +++ b/tests/GithubAdapterTest.php @@ -0,0 +1,76 @@ + + * @covers ::__construct + * @covers ::getApi + */ +class GithubAdapterTest extends \PHPUnit_Framework_TestCase +{ + ////////////////////////////////// FIXTURES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + const MOCK_FILE_PATH = '/path/to/mock/file'; + + /** @var GithubAdapter */ + private $adapter; + /** @var ApiInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $mockClient; + + /** + * + */ + protected function setup() + { + $this->mockClient = $this->getMock(ApiInterface::class); + $this->adapter = new GithubAdapter($this->mockClient); + } + + /////////////////////////////////// TESTS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @covers ::has + * @covers ::read + * @covers ::listContents + * @covers ::getMetadata + * @covers ::getSize + * @covers ::getMimetype + * @covers ::getTimestamp + * @covers ::getVisibility + * + * @dataProvider provideReadMethods + * + * @param $method + * @param $apiMethod + * @param $parameters + */ + final public function testAdapterShouldPassParameterToClient($method, $apiMethod, $parameters) + { + $mocker = $this->mockClient->expects($this->exactly(1)) + ->method($apiMethod); + + $mocker->getMatcher()->parametersMatcher = new \PHPUnit_Framework_MockObject_Matcher_Parameters($parameters); + + call_user_func_array([$this->adapter, $method], $parameters); + } + + ////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + /////////////////////////////// DATAPROVIDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + final public function provideReadMethods() + { + return [ + ['has', 'exists', [self::MOCK_FILE_PATH]], + ['read', 'getFileContents', [self::MOCK_FILE_PATH]], + ['listContents', 'getRecursiveMetadata', [self::MOCK_FILE_PATH, true]], + ['getMetadata', 'getMetadata', [self::MOCK_FILE_PATH]], + ['getSize', 'getMetadata', [self::MOCK_FILE_PATH]], + ['getMimetype', 'guessMimeType', [self::MOCK_FILE_PATH]], + ['getTimestamp', 'getLastUpdatedTimestamp', [self::MOCK_FILE_PATH]], + ['getVisibility', 'getRecursiveMetadata', [self::MOCK_FILE_PATH]], + ]; + } +} diff --git a/tests/SettingsTest.php b/tests/SettingsTest.php new file mode 100644 index 0000000..bd46a24 --- /dev/null +++ b/tests/SettingsTest.php @@ -0,0 +1,239 @@ + + * @covers ::__construct + */ +class SettingsTest extends \PHPUnit_Framework_TestCase +{ + ////////////////////////////////// FIXTURES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + const MOCK_VENDOR_NAME = 'mock_vendor'; + const MOCK_PACKAGE_NAME = 'mock_package'; + const MOCK_BRANCH = 'mock_branch'; + const MOCK_REFERENCE = 'mock_reference'; + + /** @var Settings */ + private $settings; + + /** + * + */ + final protected function setUp() + { + $this->settings = new Settings( + $this->getMockRespositoryName(), + $this->getMockCredentials(), + self::MOCK_BRANCH, + self::MOCK_REFERENCE + ); + } + + /////////////////////////////////// TESTS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @covers ::__construct + */ + final public function testSettingsShouldComplainWhenInstantiatedWithoutRepositoryName() + { + $this->setExpectedException( + \PHPUnit_Framework_Error_Warning::class, + sprintf('Missing argument %d for %s::__construct()', 1, Settings::class) + ); + + /** @noinspection PhpParamsInspection */ + new Settings(); + } + + /** + * @covers ::getRepository + */ + final public function testSettingsShouldContainRepositoryItWasGivenGivenWhenInstantiated() + { + $settings = $this->settings; + + $expected = $this->getMockRespositoryName(); + + $actual = $settings->getRepository(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::__construct + * + * @dataProvider provideInvalidRepositoryNames + * + * @param string $name + */ + final public function testSettingsShouldComplainWhenGivenInvalidRepositoryNames($name) + { + $this->setExpectedException( + \InvalidArgumentException::class, + sprintf(Settings::ERROR_INVALID_REPOSITORY_NAME, var_export($name, true)) + ); + new Settings($name); + } + + /** + * @covers ::getRepository + */ + final public function testSettingsShouldOnlyNeedRepositoryNameWhenInstantiated() + { + $settings = new Settings($this->getMockRespositoryName()); + $this->assertInstanceOf(Settings::class, $settings); + } + + /** + * @covers ::getVendor + */ + final public function testSettingsShouldContainVendorNameFromGivenRepositoryWhenInstantiated() + { + $settings = new Settings($this->getMockRespositoryName()); + + $expected = self::MOCK_VENDOR_NAME; + + $actual = $settings->getVendor(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getPackage + */ + final public function testSettingsShouldContainPackageNameFromGivenRepositoryWhenInstantiated() + { + $settings = new Settings($this->getMockRespositoryName()); + + $expected = self::MOCK_PACKAGE_NAME; + + $actual = $settings->getPackage(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getCredentials + */ + final public function testSettingsShouldContainEmptyCredentialsWhenInstantiatedWithoutCredentials() + { + $settings = new Settings($this->getMockRespositoryName()); + + $expected = []; + + $actual = $settings->getCredentials(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getCredentials + */ + final public function testSettingsShouldContainCredentialsItWasGivenGivenWhenInstantiated() + { + $settings = $this->settings; + + $expected = $this->getMockCredentials(); + + $actual = $settings->getCredentials(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getBranch + */ + final public function testSettingsShouldContainMasterAsBranchWhenInstantiatedWithoutBranch() + { + $settings = new Settings($this->getMockRespositoryName()); + + $expected = 'master'; + + $actual = $settings->getBranch(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getBranch + */ + final public function testSettingsShouldContainBranchItWasGivenGivenWhenInstantiated() + { + $settings = $this->settings; + + $expected = self::MOCK_BRANCH; + + $actual = $settings->getBranch(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getReference + */ + final public function testSettingsShouldContainHeadAsReferenceWhenInstantiatedWithoutReference() + { + $settings = new Settings($this->getMockRespositoryName()); + + $expected = 'HEAD'; + + $actual = $settings->getReference(); + + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getReference + */ + final public function testSettingsShouldContaingetReferenceItWasGivenGivenWhenInstantiated() + { + $settings = $this->settings; + + $expected = self::MOCK_REFERENCE; + + $actual = $settings->getReference(); + + $this->assertEquals($expected, $actual); + } + + ////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @return string + */ + private function getMockRespositoryName() + { + return self::MOCK_VENDOR_NAME . '/' . self::MOCK_PACKAGE_NAME; + } + + /** + * @return array + */ + private function getMockCredentials() + { + return ['mock_type', 'mock_user', 'mock_password']; + } + + /////////////////////////////// DATAPROVIDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + final public function provideInvalidRepositoryNames() + { + return [ + [''], + [null], + [true], + [array()], + ['foo'], + ['/foo'], + ['foo/bar/'], + ['/foo/bar/'], + ['foo/bar/baz'], + ['/foo/bar/baz/'], + ['foo/bar/baz/'], + ['/foo/bar/baz'], + ]; + } +} + +/*EOF*/