diff --git a/.gitignore b/.gitignore index bfcf449..d1ed533 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -build -vendor +/* Directories to ignore */ +/build +/vendor +/tests/fixtures/integration-test-repository -composer.lock -phpunit.xml +/* Files to ignore */ +.env +/composer.lock +/phpunit.xml diff --git a/.travis.yml b/.travis.yml index 093a889..90567ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,5 +19,6 @@ install: script: - vendor/bin/phpunit --configuration build/phpunit.xml && cat build/testdox.txt build/coverage.txt -after_script: +after_success: + - bash <(curl -s https://codecov.io/bash) - php vendor/bin/coveralls -v diff --git a/CHANGELOG.md b/CHANGELOG.md index c386634..95b6526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,30 +13,57 @@ and [Semantic Versioning](http://semver.org/) conventions. ### Security --> -## v0.2.0 - 2015-07-21 - Improvements and UnitTests +## [v0.3.0] - 2016-04-20 - Metadata for directories and various other bugfixes ### Added + +- Adds various @throws annotations +- Adds timestamps for files and folders +- Adds integration tests to validate output against comparable output from LocalAdapter + +### Changed + +- Changes the default for file visibility to visible +- Changes guessing MIME type for files to always use the file's content instead of extension to guarantee results are the same across different filesystem adapters +- Changes method name of ApiInterface::getRecursiveMetadata() to ApiInterface::getTreeMetadata() + +### Fixed + +- Fixes bug that caused invalid repository names to be accepted (issue #4) +- Fixes bug that caused incorrect Metadata for directories to be returned (issue #6) +- Fixes bug that didn't validate paths ended in a trailing slash (issue #8) +- Fixes bug in permission comparison +- Fixes various links in the README file + +## [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 +## [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 ### Added + - 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.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 +[unreleased]: https://github.com/potherca/flysystem-github/compare/v0.3.0...HEAD +[v0.3.0]: https://github.com/potherca/flysystem-github/compare/v0.2.0...v0.3.0 +[v0.2.0]: https://github.com/potherca/flysystem-github/compare/v0.1.0...v0.2.0 +[v0.1.0]: https://github.com/potherca/flysystem-github/compare/v0.0.0...v0.1.0 [keep-a-changelog]: http://keepachangelog.com/ [Semantic Versioning]: http://semver.org/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1631d2b..947f2e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,13 @@ Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [Github](https://github.com/potherca/flysystem-github). - ## Pull Requests - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). - **Add tests!** - Your patch won't be accepted if it doesn't have tests. -- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. +- **Document any change in behaviour** - Make sure the `README.md`, `CHANGELOG.md` and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. @@ -21,7 +20,6 @@ We accept contributions via Pull Requests on [Github](https://github.com/potherc - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. - ## Running Tests ``` bash diff --git a/README.md b/README.md index 854f5c7..8eb6839 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Flysystem Adapter for Github -[![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.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) +[![Latest Version][Latest Version Badge]][Release Page] +[![Software License][Software License Badge]][License file] +[![Build Status][Build Status Badge]][Travis Page] +[![Coverage Status][Coverage Status Badge]][Coveralls Page] +[![Quality Score][Quality Score Badge]][Scrutinizer Page] +[![Total Downloads][Total Downloads Badge]][Packagist Page] ## Install @@ -89,26 +89,59 @@ $filesystem = new Filesystem($adapter); ## Testing +The unit-tests can be run with the following command: + ``` bash $ composer test ``` +To run integration tests, which use the Github API, a [Github API token] might be needed (to stop the tests hitting the API Limit). +An API key can be added by setting it in the environment as `GITHUB_API_KEY` or by creating an `.env` file in the integration tests directory and setting it there. +See `tests/integration-tests/.env.example` for an example. + +To run the integration test, run the following command (this will also run the unit-tests): + +``` bash +$ composer test-all +``` + ## 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. +Please see [CONTRIBUTING] for details. ## Change Log -Please see [CHANGELOG](CHANGELOG.md) for details. +Please see [CHANGELOG] for details. ## Credits -- [Potherca](https://github.com/potherca) +- [Potherca] +- [Contributors] ## License -The MIT License (MIT). Please see [License File](LICENSE.md) for more information. +The MIT License (MIT). Please see [License File] for more information. + +[Release Page]: https://github.com/potherca/flysystem-github/releases +[License File]: LICENSE.md +[Travis Page]: https://travis-ci.org/Potherca/flysystem-github +[Coveralls Page]: https://coveralls.io/github/potherca/flysystem-github +[Scrutinizer Page]: https://scrutinizer-ci.com/g/potherca/flysystem-github +[Packagist Page]: https://packagist.org/packages/potherca/flysystem-github + +[Latest Version Badge]: https://img.shields.io/github/release/potherca/flysystem-github.svg +[Software License Badge]: https://img.shields.io/badge/license-MIT-brightgreen.svg +[Build Status Badge]: https://img.shields.io/travis/Potherca/flysystem-github.svg +[Coverage Status Badge]: https://coveralls.io/repos/potherca/flysystem-github/badge.svg +[Quality Score Badge]: https://img.shields.io/scrutinizer/g/potherca/flysystem-github.svg +[Total Downloads Badge]: https://img.shields.io/packagist/dt/potherca/flysystem-github.svg + +[Contributors]: https://github.com/Potherca/flysystem-github/graphs/contributors +[CHANGELOG]: CHANGELOG.md +[CONTRIBUTING]: CONTRIBUTING.md +[Potherca]: https://github.com/potherca +[Github API token]: https://help.github.com/articles/creating-an-access-token-for-command-line-use/ \ No newline at end of file diff --git a/build/phpunit.xml b/build/phpunit.xml index df9f636..5de965e 100644 --- a/build/phpunit.xml +++ b/build/phpunit.xml @@ -1,14 +1,20 @@ - - ../tests + + ../tests/unit-tests + + + ../tests/integration-tests + src/ + diff --git a/composer.json b/composer.json index bc1dfeb..5257566 100644 --- a/composer.json +++ b/composer.json @@ -20,15 +20,17 @@ } ], "require": { - "php" : ">=5.5", + "php" : ">=5.4", "knplabs/github-api": "^1.4", "league/flysystem": "^1.0" }, "require-dev": { - "phpunit/phpunit" : "^4.7.7", - "satooshi/php-coveralls": "^0.6.1", - "scrutinizer/ocular": "^1.1", - "whatthejeff/nyancat-phpunit-resultprinter": "^1.2" + "php" : ">=5.5", + "josegonzalez/dotenv": "~2.0", + "phpunit/phpunit" : "~4.8", + "satooshi/php-coveralls": "~0.6", + "scrutinizer/ocular": "~1.1", + "whatthejeff/nyancat-phpunit-resultprinter": "~1.2" }, "autoload": { "psr-4": { @@ -46,6 +48,7 @@ } }, "scripts": { - "test" : "phpunit" + "test" : "phpunit", + "test-all" : "phpunit --configuration build/phpunit.xml" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9b8dda9..f04bd2b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,21 @@ - - tests + + tests/unit-tests diff --git a/src/Api.php b/src/Api.php index c5fbbef..2919ddf 100644 --- a/src/Api.php +++ b/src/Api.php @@ -4,6 +4,7 @@ use Github\Api\GitData; use Github\Api\Repo; +use Github\Api\Repository\Contents; use Github\Client; use Github\Exception\RuntimeException; use League\Flysystem\AdapterInterface; @@ -15,6 +16,7 @@ class Api implements ApiInterface { ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ + const ERROR_NO_NAME = 'Could not set name for entry'; const ERROR_NOT_FOUND = 'Not Found'; const API_GIT_DATA = 'git'; @@ -34,10 +36,16 @@ class Api implements ApiInterface const KEY_TREE = 'tree'; const KEY_TYPE = 'type'; const KEY_VISIBILITY = 'visibility'; - const ERROR_NO_NAME = 'Could not set name for entry'; + + const GITHUB_API_URL = 'https://api.github.com'; + const GITHUB_URL = 'https://github.com'; + + const MIME_TYPE_DIRECTORY = 'directory'; /** @var Client */ private $client; + /** @var Contents */ + private $contents; /** @var SettingsInterface */ private $settings; /** @var bool */ @@ -46,7 +54,10 @@ class Api implements ApiInterface //////////////////////////// SETTERS AND GETTERS \\\\\\\\\\\\\\\\\\\\\\\\\\\ /** * @param string $name + * * @return \Github\Api\ApiInterface + * + * @throws \Github\Exception\InvalidArgumentException */ private function getApi($name) { @@ -56,6 +67,8 @@ private function getApi($name) /** * @return GitData + * + * @throws \Github\Exception\InvalidArgumentException */ private function getGitDataApi() { @@ -64,6 +77,8 @@ private function getGitDataApi() /** * @return Repo + * + * @throws \Github\Exception\InvalidArgumentException */ private function getRepositoryApi() { @@ -72,10 +87,15 @@ private function getRepositoryApi() /** * @return \Github\Api\Repository\Contents + * + * @throws \Github\Exception\InvalidArgumentException */ private function getRepositoryContent() { - return $this->getRepositoryApi()->contents(); + if ($this->contents === null) { + $this->contents = $this->getRepositoryApi()->contents(); + } + return $this->contents; } //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ @@ -93,6 +113,8 @@ final public function __construct(Client $client, SettingsInterface $settings) * @param string $path * * @return bool + * + * @throws \Github\Exception\InvalidArgumentException */ final public function exists($path) { @@ -108,6 +130,7 @@ final public function exists($path) * @param $path * * @return null|string + * @throws \Github\Exception\InvalidArgumentException * * @throws \Github\Exception\ErrorException */ @@ -125,6 +148,8 @@ final public function getFileContents($path) * @param string $path * * @return array + * + * @throws \Github\Exception\InvalidArgumentException */ final public function getLastUpdatedTimestamp($path) { @@ -141,6 +166,8 @@ final public function getLastUpdatedTimestamp($path) * @param string $path * * @return array + * + * @throws \Github\Exception\InvalidArgumentException */ final public function getCreatedTimestamp($path) { @@ -157,6 +184,9 @@ final public function getCreatedTimestamp($path) * @param string $path * * @return array|bool + * + * @throws \Github\Exception\InvalidArgumentException + * @throws \Github\Exception\RuntimeException */ final public function getMetaData($path) { @@ -175,6 +205,37 @@ final public function getMetaData($path) } } + if (is_array($metadata) === true && $this->isMetadataForDirectory($metadata) === true) { + /** @var $metadata array */ + $project = sprintf('%s/%s', $this->settings->getVendor(), $this->settings->getPackage()); + $reference = $this->settings->getReference(); + + $url = sprintf( + '%s/repos/%s/contents/%s?ref=%s', + self::GITHUB_API_URL, + $project, + trim($path, '/'), + $reference + ); + $htmlUrl = sprintf( + '%s/%s/blob/%s/%s', + self::GITHUB_URL, + $project, + $reference, + trim($path, '/') + ); + + $metadata = [ + self::KEY_TYPE => self::KEY_DIRECTORY, + 'url' => $url, + 'html_url' => $htmlUrl, + '_links' => [ + 'self' => $url, + 'html' => $htmlUrl + ] + ]; + } + return $metadata; } @@ -183,36 +244,66 @@ final public function getMetaData($path) * @param bool $recursive * * @return array + * @throws \Github\Exception\InvalidArgumentException */ - final public function getRecursiveMetadata($path, $recursive) + final public function getTreeMetadata($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, + // exceeded the github maximum limit. If we 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 + true //@NOTE: To retrieve all needed date the 'recursive' flag should always be 'true' ); + $path = rtrim($path, '/') . '/'; + $treeMetadata = $this->extractMetaDataFromTreeInfo($info[self::KEY_TREE], $path, $recursive); - return $this->normalizeTreeMetadata($treeMetadata); + $normalizeTreeMetadata = $this->normalizeTreeMetadata($treeMetadata); + + $directoryTimestamp = 0000000000; + + array_walk($normalizeTreeMetadata, function (&$entry) use (&$directoryTimestamp) { + if ($this->hasKey($entry, self::KEY_TIMESTAMP) === false + || $entry[self::KEY_TIMESTAMP] === false + ) { + $timestamp = $this->getCreatedTimestamp($entry[self::KEY_PATH])['timestamp']; + + $entry[self::KEY_TIMESTAMP] = $timestamp; + + if ($timestamp > $directoryTimestamp) { + $directoryTimestamp = $timestamp; + } + } + }); + + /* @FIXME: It might be wise to use a filter to find the right entry instead of always using the first entry in the array. */ + + $normalizeTreeMetadata[0]['timestamp'] = $directoryTimestamp; + + return $normalizeTreeMetadata; } /** * @param string $path * * @return null|string + * + * @throws \Github\Exception\ErrorException + * @throws \Github\Exception\InvalidArgumentException + * @throws \Github\Exception\RuntimeException */ 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'; + $meta = $this->getMetaData($path); + + if ($this->hasKey($meta, self::KEY_TYPE) && $meta[self::KEY_TYPE] === self::KEY_DIRECTORY) { + $mimeType = self::MIME_TYPE_DIRECTORY; // or application/x-directory } else { $content = $this->getFileContents($path); $mimeType = MimeType::detectByContent($content); @@ -224,6 +315,7 @@ final public function guessMimeType($path) ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ /** * + * @throws \Github\Exception\InvalidArgumentException If no authentication method was given */ private function authenticate() { @@ -255,30 +347,26 @@ private function authenticate() */ 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); - } + $matchPath = substr($path, 0, -1); + $length = abs(strlen($matchPath) - 1); + + $metadata = array_filter($tree, function ($entry) use ($matchPath, $recursive, $length) { + $match = false; + + $entryPath = $entry[self::KEY_PATH]; + + if ($matchPath === '' || strpos($entryPath, $matchPath) === 0) { + if ($recursive === true) { + $match = true; + } else { + $match = ($matchPath !== '' || strpos($entryPath, '/', $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 $match; + }); - return $metadata; + return array_values($metadata); } /** @@ -287,7 +375,13 @@ private function extractMetaDataFromTreeInfo(array $tree, $path, $recursive) */ private function guessVisibility($permissions) { - return $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; + $visibility = AdapterInterface::VISIBILITY_PUBLIC; + + if (! substr($permissions, -4) & 0044) { + $visibility = AdapterInterface::VISIBILITY_PRIVATE; + } + + return $visibility; } /** @@ -323,6 +417,8 @@ private function normalizeTreeMetadata($metadata) * @param $path * * @return array + * + * @throws \Github\Exception\InvalidArgumentException */ private function commitsForFile($path) { @@ -345,7 +441,7 @@ private function commitsForFile($path) */ private function setDefaultValue(array &$entry, $key, $default = false) { - if (isset($entry[$key]) === false) { + if ($this->hasKey($entry, $key) === false) { $entry[$key] = $default; } } @@ -355,7 +451,7 @@ private function setDefaultValue(array &$entry, $key, $default = false) */ private function setEntryType(&$entry) { - if (isset($entry[self::KEY_TYPE]) === true) { + if ($this->hasKey($entry, self::KEY_TYPE) === true) { switch ($entry[self::KEY_TYPE]) { case self::KEY_BLOB: $entry[self::KEY_TYPE] = self::KEY_FILE; @@ -373,10 +469,11 @@ private function setEntryType(&$entry) */ private function setEntryVisibility(&$entry) { - if (isset($entry[self::KEY_MODE])) { + if ($this->hasKey($entry, self::KEY_MODE)) { $entry[self::KEY_VISIBILITY] = $this->guessVisibility($entry[self::KEY_MODE]); } else { - $entry[self::KEY_VISIBILITY] = false; + /* Assume public by default */ + $entry[self::KEY_VISIBILITY] = GithubAdapter::VISIBILITY_PUBLIC; } } @@ -385,14 +482,48 @@ private function setEntryVisibility(&$entry) */ private function setEntryName(&$entry) { - if (isset($entry[self::KEY_NAME]) === false) { - if (isset($entry[self::KEY_FILENAME]) === true) { + if ($this->hasKey($entry, self::KEY_NAME) === false) { + if ($this->hasKey($entry, self::KEY_FILENAME) === true) { $entry[self::KEY_NAME] = $entry[self::KEY_FILENAME]; - } elseif (isset($entry[self::KEY_PATH]) === true) { + } elseif ($this->hasKey($entry, self::KEY_PATH) === true) { $entry[self::KEY_NAME] = $entry[self::KEY_PATH]; } else { $entry[self::KEY_NAME] = null; } } } + + /** + * @param $metadata + * @return bool + */ + private function isMetadataForDirectory($metadata) + { + $isDirectory = false; + + $keys = array_keys($metadata); + + if ($keys[0] === 0) { + $isDirectory = true; + } + + return $isDirectory; + } + + /** + * @param $subject + * @param $key + * @return mixed + */ + private function hasKey(&$subject, $key) + { + $keyExists = false; + + if (is_array($subject)) { + /** @noinspection ReferenceMismatchInspection */ + $keyExists = array_key_exists($key, $subject); + } + + return $keyExists; + } } diff --git a/src/ApiInterface.php b/src/ApiInterface.php index d8cb101..d5168de 100644 --- a/src/ApiInterface.php +++ b/src/ApiInterface.php @@ -42,7 +42,7 @@ public function getMetaData($path); * * @return array */ - public function getRecursiveMetadata($path, $recursive); + public function getTreeMetadata($path, $recursive); /** * @param string $path diff --git a/src/GithubAdapter.php b/src/GithubAdapter.php index f48345f..1379ecc 100644 --- a/src/GithubAdapter.php +++ b/src/GithubAdapter.php @@ -169,6 +169,8 @@ public function has($path) * @param string $path * * @return array|false + * + * @throws \Github\Exception\ErrorException */ public function read($path) { @@ -185,7 +187,13 @@ public function read($path) */ public function listContents($path = '/', $recursive = false) { - return $this->getApi()->getRecursiveMetadata($path, $recursive); + $contents = $this->getApi()->getTreeMetadata($path, $recursive); + + if ($this->isDirectoryContents($contents) === false) { + $contents = []; + } + + return $contents; } /** @@ -246,9 +254,26 @@ public function getTimestamp($path) public function getVisibility($path) { $recursive = false; - $metadata = $this->getApi()->getRecursiveMetadata($path, $recursive); + $metadata = $this->getApi()->getTreeMetadata($path, $recursive); return $metadata[0]; } + + /** + * @param $contents + * @return bool + */ + private function isDirectoryContents($contents) + { + $isDirectory = false; + + if (is_array($contents)) { + $isDirectory = array_key_exists(Api::KEY_TYPE, $contents) === false + || $contents[Api::KEY_TYPE] === Api::KEY_DIRECTORY + ; + } + + return $isDirectory; + } } /*EOF*/ diff --git a/src/Settings.php b/src/Settings.php index 8ea14b1..faeae5d 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -101,9 +101,7 @@ final public function __construct( private function isValidRepositoryName($repository) { if (is_string($repository) === false - || strpos($repository, '/') === false - || strpos($repository, '/') === 0 - || substr_count($repository, '/') !== 1 + || preg_match('#^[^/]+/[^/]+$#', $repository) !== 1 ) { $message = sprintf( self::ERROR_INVALID_REPOSITORY_NAME, diff --git a/tests/GithubAdapterTest.php b/tests/GithubAdapterTest.php deleted file mode 100644 index 54c10af..0000000 --- a/tests/GithubAdapterTest.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @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/fixtures/listContents-folder-recursive.json b/tests/fixtures/listContents-folder-recursive.json new file mode 100644 index 0000000..785a69b --- /dev/null +++ b/tests/fixtures/listContents-folder-recursive.json @@ -0,0 +1,46 @@ +{ + "sha": "676a09fd1c7df28938e8c12dc5d9f3c3271f0249", + "url": "https://api.github.com/repos/potherca-bot/test-repository/git/trees/676a09fd1c7df28938e8c12dc5d9f3c3271f0249", + "tree": [ + { + "path": "README", + "mode": "100755", + "type": "blob", + "sha": "1ff3a296caf2d27828dd8c40673c88dbf99d4b3a", + "size": 58, + "url": "https://api.github.com/repos/potherca-bot/test-repository/git/blobs/1ff3a296caf2d27828dd8c40673c88dbf99d4b3a" + }, + { + "path": "a-directory", + "mode": "040000", + "type": "tree", + "sha": "30b7e362894eecb159ce0ba2921a8363cd297213", + "url": "https://api.github.com/repos/potherca-bot/test-repository/git/trees/30b7e362894eecb159ce0ba2921a8363cd297213" + }, + { + "path": "a-directory/another-file.js", + "mode": "100755", + "type": "blob", + "sha": "f542363e1b45aa7a33e5e731678dee18f7a1e729", + "size": 52, + "url": "https://api.github.com/repos/potherca-bot/test-repository/git/blobs/f542363e1b45aa7a33e5e731678dee18f7a1e729" + }, + { + "path": "a-directory/readme.txt", + "mode": "100755", + "type": "blob", + "sha": "27f8ec8435cb07992ecf18f9d5494ffc14948368", + "size": 31, + "url": "https://api.github.com/repos/potherca-bot/test-repository/git/blobs/27f8ec8435cb07992ecf18f9d5494ffc14948368" + }, + { + "path": "a-file.php", + "mode": "100755", + "type": "blob", + "sha": "c6e6cd91e3ae40ab74883720a0d6cfb2af89e4b1", + "size": 117, + "url": "https://api.github.com/repos/potherca-bot/test-repository/git/blobs/c6e6cd91e3ae40ab74883720a0d6cfb2af89e4b1" + } + ], + "truncated": false +} \ No newline at end of file diff --git a/tests/integration-tests/.env.example b/tests/integration-tests/.env.example new file mode 100644 index 0000000..7fefd21 --- /dev/null +++ b/tests/integration-tests/.env.example @@ -0,0 +1,2 @@ +# Github API key used by integration tests +GITHUB_API_KEY='abcdef0123456789abcdef0123456789abcdef01' \ No newline at end of file diff --git a/tests/integration-tests/compareToLocalCalls.php b/tests/integration-tests/compareToLocalCalls.php new file mode 100644 index 0000000..720825f --- /dev/null +++ b/tests/integration-tests/compareToLocalCalls.php @@ -0,0 +1,181 @@ +filesystem = new Filesystem(new GithubAdapter(new Api(new Client(), new Settings($project, $credentials)))); + } + + /////////////////////////////////// TESTS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @param string $function + * @param array $parameters + * @param array $file + * + * @dataProvider provideFiles + */ + final public function testOutputMatchesLocalAdapterOutputWhenCalledOnTheSameSource($function, array $parameters, array $file) + { + $path = $file['path']; + + $localFileSystem = $this->getLocalFileSystem(); + + $localResult = $localFileSystem->{$function}($path); + $result = $this->filesystem->{$function}($path); + + if (array_key_exists('callback', $parameters)) { + $parameters['callback']($localResult, $result); + } else { + self::assertEquals($localResult, $result); + } + } + + /////////////////////////////// DATAPROVIDERS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + final public function provideFiles() + { + $this->createFixture(); + + $files = []; + + $localFileSystem = $this->getLocalFileSystem(); + + // @TODO: Test for FileNotFoundException + //$files[] = ['path'=>'NON-EXISTENT-FILE']; + + $functions = [ + 'assertPresent' => [], + 'get' => ['callback' => function ($localResult, $result) { + if ($localResult instanceof \League\Flysystem\Directory) { + /** @var $localResult \League\Flysystem\Directory */ + /** @var $result \League\Flysystem\Directory */ + $localContents = $localResult->getContents(); + /** @var array $contents */ + $contents = $result->getContents(); + $this->compare($localContents, $contents); + } elseif ($localResult instanceof \League\Flysystem\File) { + /** @var $localResult \League\Flysystem\File */ + /** @var $result \League\Flysystem\File */ + self::assertEquals($localResult->read(), $result->read()); + } else { + self::assertEquals($localResult, $result); + } + }], + 'getMimetype' => [], + 'getSize' => [], + //@FIXME: Synchronize local timestamp with remote git repo timestamp so "getTimestamp" can be tested + // 'getTimestamp' => [], + 'getVisibility' => [], + 'has' => [], + 'listContents' => ['type' => 'dir', 'callback' => [$this, 'compare']], + 'read' => ['type' => 'file'], + 'readStream' => ['type' => 'file', 'callback' => function ($localStream, $githubStream){ + self::assertEquals(stream_get_contents($localStream), stream_get_contents($githubStream)); + }], + ]; + + $localFiles = $localFileSystem->listContents('', true); + + foreach ($localFiles as $index => $file) { + + $path = $file['path']; + + if (strpos($path, '.') !== 0) { + foreach ($functions as $function => $parameters) { + if (array_key_exists('type', $parameters) === false || $file['type'] === $parameters['type']) { + $key = sprintf('%s - %s', $function, $path); + $files[$key] = [$function, $parameters, $file]; + } + } + } + } + ksort($files); + + return $files; + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + /** + * @param $envFilePath + */ + private static function loadEnvironmentalVariables($envFilePath) + { + if (is_file($envFilePath)) { + $loader = (new josegonzalez\Dotenv\Loader($envFilePath)); + $loader->parse()->putenv(); + } + } + + private function createFixture() + { + $gitRemote = $this->gitRemote; + $fixturesPath = $this->getFixturePath(); + $glob = glob($fixturesPath.'**'); + + if (is_dir($fixturesPath) === false || count($glob) === 0) { + + fwrite(STDERR, sprintf('Creating fixture directory from %s.%s', $gitRemote, "\n")); + + exec(sprintf('git clone %s %s', $gitRemote, $fixturesPath)); + } + } + + /** + * @param array $localContents + * @param array $contents + */ + private function compare(array $localContents, array $contents) + { + array_walk($contents, 'ksort'); + array_walk($localContents, 'ksort'); + + $localContents = array_map(function ($value) { + unset($value['timestamp']); + return $value; + }, $localContents); + + foreach ($localContents as $index => $localContent) { + foreach ($localContent as $key => $value) { + self::assertEquals($value, $contents[$index][$key]); + } + } + } + + private function getLocalFileSystem() + { + return new Filesystem(new LocalAdapter($this->getFixturePath())); + } + + /** + * @return string + */ + private function getFixturePath() + { + return dirname(__DIR__) . '/fixtures/integration-test-repository/'; + } +} + +/*EOF*/ diff --git a/tests/ApiTest.php b/tests/unit-tests/ApiTest.php similarity index 60% rename from tests/ApiTest.php rename to tests/unit-tests/ApiTest.php index f57ecf0..c1c3a19 100644 --- a/tests/ApiTest.php +++ b/tests/unit-tests/ApiTest.php @@ -8,6 +8,7 @@ use Github\Api\Repository\Contents; use Github\Client; use Github\Exception\RuntimeException; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Tests for the Api class @@ -21,12 +22,13 @@ class ApiTest extends \PHPUnit_Framework_TestCase ////////////////////////////////// FIXTURES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ const MOCK_FILE_PATH = '/path/to/mock/file'; const MOCK_FILE_CONTENTS = 'Mock file contents'; + const MOCK_FOLDER_PATH = 'a-directory'; /** @var Api */ private $api; - /** @var Client|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Client|MockObject */ private $mockClient; - /** @var Settings|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Settings|MockObject */ private $mockSettings; /** @@ -53,10 +55,7 @@ final public function testApiShouldComplainWhenInstantiatedWithoutClient() Client::class ); - $this->setExpectedException( - \PHPUnit_Framework_Error::class, - $message - ); + $this->setExpectedException(\PHPUnit_Framework_Error::class, $message); /** @noinspection PhpParamsInspection */ new Api(); @@ -74,10 +73,7 @@ final public function testApiShouldComplainWhenInstantiatedWithoutSettings() SettingsInterface::class ); - $this->setExpectedException( - \PHPUnit_Framework_Error::class, - $message - ); + $this->setExpectedException(\PHPUnit_Framework_Error::class, $message); /** @noinspection PhpParamsInspection */ new Api($this->getMockClient()); @@ -111,7 +107,7 @@ final public function testApiShouldUseValuesFromSettingsWhenAskingClientForFileC $actual = $api->getFileContents(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } /** @@ -142,7 +138,7 @@ final public function testApiShouldUseValuesFromSettingsWhenAskingClientIfFileEx $actual = $api->exists(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } /** @@ -158,7 +154,7 @@ final public function testApiShouldUseValuesFromSettingsWhenAskingClientForLastU $actual = $api->getLastUpdatedTimestamp(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } /** @@ -174,8 +170,9 @@ final public function testApiShouldUseValuesFromSettingsWhenAskingClientForCreat $actual = $api->getCreatedTimestamp(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } + /** * @covers ::getMetaData */ @@ -204,7 +201,7 @@ final public function testApiShouldUseValuesFromSettingsWhenAskingClientForFileI $actual = $api->getMetaData(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } /** @@ -216,13 +213,72 @@ final public function testApiShouldAccountForFileNotExistingWhenAskingInfoForFil $expected = false; - $this->mockClient->expects($this->exactly(1)) - ->method('api') - ->willThrowException(new RuntimeException(Api::ERROR_NOT_FOUND)); + $this->mockClient->expects(self::exactly(1)) + ->method('api') + ->willThrowException(new RuntimeException(Api::ERROR_NOT_FOUND)); $actual = $api->getMetaData(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); + } + + /** + * @covers ::getMetaData + */ + final public function testApiShouldReturnMetadataForDirectoryWhenGivenPathIsDirectory() + { + $api = $this->api; + + $mockPackage = 'mockPackage'; + $mockPath = self::MOCK_FOLDER_PATH; + $mockReference = 'mockReference'; + $mockVendor = 'mockVendor'; + + $expectedUrl = sprintf( + '%s/repos/%s/%s/contents/%s?ref=%s', + $api::GITHUB_API_URL, + $mockVendor, + $mockPackage, + $mockPath, + $mockReference + ); + + $expectedHtmlUrl = sprintf( + '%s/%s/%s/blob/%s/%s', + $api::GITHUB_URL, + $mockVendor, + $mockPackage, + $mockReference, + $mockPath + ); + + $expected = [ + 'type' => $api::KEY_DIRECTORY, + 'url' => $expectedUrl, + 'html_url' => $expectedHtmlUrl, + '_links' => Array ( + 'self' => $expectedUrl, + 'html' => $expectedHtmlUrl, + ), + ]; + + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $this->prepareMockApi( + 'show', + $api::API_REPO, + [$mockVendor, $mockPackage, $mockPath, $mockReference], + [0 => null] + ); + + $actual = $api->getMetaData($mockPath); + + self::assertEquals($expected, $actual); } /** @@ -236,13 +292,13 @@ final public function testApiShouldPassOtherRuntimeExceptionsWhenAskingInfoForFi $expected = false; - $this->mockClient->expects($this->exactly(1)) - ->method('api') - ->willThrowException(new RuntimeException(self::MOCK_FILE_CONTENTS)); + $this->mockClient->expects(self::exactly(1)) + ->method('api') + ->willThrowException(new RuntimeException(self::MOCK_FILE_CONTENTS)); $actual = $api->getMetaData(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } /** @@ -256,31 +312,25 @@ final public function testApiShouldPassOnExceptionsWhenAskingInfoForFileCausesAn $expected = false; - $this->mockClient->expects($this->exactly(1)) - ->method('api') - ->willThrowException(new \RuntimeException(Api::ERROR_NOT_FOUND)); + $this->mockClient->expects(self::exactly(1)) + ->method('api') + ->willThrowException(new \RuntimeException(Api::ERROR_NOT_FOUND)); $actual = $api->getMetaData(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); } /** - * @covers ::getRecursiveMetadata + * @covers ::getTreeMetadata + * + * @uses Potherca\Flysystem\Github\Api::getCreatedTimestamp * * @dataProvider provideExpectedMetadata * - * @param string $path - * @param array $expected - * @param bool $recursive - * @param bool $truncated + * @param array $data */ - final public function testApiShouldRetrieveExpectedMetadataWhenAskedTogetRecursiveMetadata( - $path, - $expected, - $recursive, - $truncated - ) { + final public function testApiShouldRetrieveExpectedMetadataWhenAskedToGetTreeMetadata($data) { $api = $this->api; $mockVendor = 'vendor'; @@ -296,40 +346,27 @@ final public function testApiShouldRetrieveExpectedMetadataWhenAskedTogetRecursi $this->prepareMockApi( 'show', $api::API_GIT_DATA, - [$mockVendor, $mockPackage, $mockReference, $recursive], - $this->getMockApiTreeResponse($truncated, $api), + [$mockVendor, $mockPackage, $mockReference, true], + $this->getMockApiTreeResponse($data['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'; + $actual = $api->getTreeMetadata($data['path'], $data['recursive']); - $this->mockClient->expects($this->never())->method('api'); + $actual = array_map(function ($value) { + $value['timestamp'] = null; + return $value; + }, $actual); - $actual = $api->guessMimeType(self::MOCK_FILE_PATH.'.png'); - - $this->assertEquals($expected, $actual); + self::assertEquals($data['expected'], $actual); } /** * @covers ::guessMimeType * * @uses League\Flysystem\Util\MimeType - * * @uses Potherca\Flysystem\Github\Api::getFileContents + * @uses Potherca\Flysystem\Github\Api::getMetaData */ final public function testApiShouldUseFileContentsToGuessMimeTypeWhenExtensionUnavailable() { @@ -363,7 +400,42 @@ final public function testApiShouldUseFileContentsToGuessMimeTypeWhenExtensionUn $actual = $api->guessMimeType(self::MOCK_FILE_PATH); - $this->assertEquals($expected, $actual); + self::assertEquals($expected, $actual); + } + + /** + * @covers ::guessMimeType + * + * @uses League\Flysystem\Util\MimeType + * @uses Potherca\Flysystem\Github\Api::getFileContents + * @uses Potherca\Flysystem\Github\Api::getMetaData + */ + final public function testApiShouldGuessMimeTypeCorrectlyWhenGivenPathIsDirectory() + { + $api = $this->api; + + $expected = $api::MIME_TYPE_DIRECTORY; + + $mockVendor = 'vendor'; + $mockPackage = 'package'; + $mockReference = 'reference'; + + $this->prepareMockSettings([ + 'getVendor' => $mockVendor, + 'getPackage' => $mockPackage, + 'getReference' => $mockReference, + ]); + + $this->prepareMockApi( + 'show', + $api::API_REPO, + [$mockVendor, $mockPackage, self::MOCK_FOLDER_PATH, $mockReference], + [0 => [$api::KEY_TYPE => $api::MIME_TYPE_DIRECTORY]] + ); + + $actual = $api->guessMimeType(self::MOCK_FOLDER_PATH); + + self::assertEquals($expected, $actual); } /** @@ -391,7 +463,7 @@ final public function testApiShouldUseCredentialsWhenTheyHaveBeenGiven() '' ); - $this->mockClient->expects($this->exactly(1)) + $this->mockClient->expects(self::exactly(1)) ->method('authenticate') ; @@ -400,7 +472,7 @@ final public function testApiShouldUseCredentialsWhenTheyHaveBeenGiven() ////////////////////////////// MOCKS AND STUBS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ /** - * @return Client|\PHPUnit_Framework_MockObject_MockObject + * @return Client|MockObject */ private function getMockClient() { @@ -410,7 +482,7 @@ private function getMockClient() } /** - * @return Settings|\PHPUnit_Framework_MockObject_MockObject + * @return Settings|MockObject */ private function getMockSettings() { @@ -425,14 +497,27 @@ private function getMockSettings() * @param mixed $apiOutput * @param string $repositoryClass */ - private function prepareMockApi($method, $apiName, $apiParameters, $apiOutput, $repositoryClass = Contents::class) - { + private function prepareMockApi( + $method, + $apiName, + $apiParameters, + $apiOutput, + $repositoryClass = Contents::class + ) { $parts = explode('\\', $repositoryClass); $repositoryName = strtolower(array_pop($parts)); + $methods = [$repositoryName, 'getPerPage', 'setPerPage']; + + $shouldMockCommitsRepository = false; + if (in_array('commits', $methods, true) === false) { + $shouldMockCommitsRepository = true; + $methods[] = 'commits'; + } + $mockApi = $this->getMockBuilder(ApiInterface::class) - ->setMethods([$repositoryName, 'getPerPage', 'setPerPage']) + ->setMethods($methods) ->getMock() ; @@ -441,23 +526,45 @@ private function prepareMockApi($method, $apiName, $apiParameters, $apiOutput, $ ->getMock() ; - $mockRepository->expects($this->exactly(1)) + $mockRepository->expects(self::exactly(1)) ->method($method) ->withAnyParameters() ->willReturnCallback(function () use ($apiParameters, $apiOutput) { - $this->assertEquals($apiParameters, func_get_args()); + self::assertEquals($apiParameters, func_get_args()); return $apiOutput; }) ; - $mockApi->expects($this->exactly(1)) + $mockApi->expects(self::exactly(1)) ->method($repositoryName) ->willReturn($mockRepository) ; - $this->mockClient->expects($this->exactly(1)) + if ($shouldMockCommitsRepository === true) { + $mockCommitsRepository = $this->getMockBuilder(Commits::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $apiOutput = [ + ['commit' => ['committer' => ['date' => '20150101']]], + ['commit' => ['committer' => ['date' => '20140202']]] + ]; + + $mockCommitsRepository->expects(self::any()) + ->method('all') + ->withAnyParameters() + ->willReturn($apiOutput) + ; + $mockApi->expects(self::any()) + ->method('commits') + ->willReturn($mockCommitsRepository) + ; + } + + $this->mockClient->expects(self::any()) ->method('api') - ->with($apiName) + ->with(self::matchesRegularExpression(sprintf('/%s|repo/', $apiName))) ->willReturn($mockApi) ; } @@ -468,7 +575,7 @@ private function prepareMockApi($method, $apiName, $apiParameters, $apiOutput, $ private function prepareMockSettings(array $expectations) { foreach ($expectations as $methodName => $returnValue) { - $this->mockSettings->expects($this->exactly(1)) + $this->mockSettings->expects(self::any()) ->method($methodName) ->willReturn($returnValue) ; @@ -476,11 +583,11 @@ private function prepareMockSettings(array $expectations) } /** - * @param $truncated - * @param $api + * @param bool $truncated + * @param Api $api * @return array */ - private function getMockApiTreeResponse($truncated, $api) + private function getMockApiTreeResponse($truncated, Api $api) { return [ $api::KEY_TREE => [ @@ -561,239 +668,274 @@ private function prepareFixturesForTimeStamp() final public function provideExpectedMetadata() { return [ - 'Filepath, not recursive, not truncated' => [ - self::MOCK_FILE_PATH, - [ + 'Filepath, not recursive, not truncated' => [[ + 'path' => self::MOCK_FILE_PATH, + 'expected' => [ [ 'path' => '/path/to/mock/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'dir', 'size' => 57, 'name' => '/path/to/mock/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/fileFoo', 'basename' => '/path/to/mock/fileFoo', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/fileFoo', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' - ] + ], + [ + 'path' => '/path/to/mock/file/Bar', + 'name' => '/path/to/mock/file/Bar', + 'mode' => '100644', + 'type' => 'file', + 'size' => 57, + 'visibility' => 'public', + 'contents' => false, + 'stream' => false, + 'timestamp' => null + ], ], - false, - false - ], - 'Filepath, recursive, not truncated' => [ - self::MOCK_FILE_PATH, - [ + 'recursive' => false, + 'truncated' => false, + ]], + 'Filepath, recursive, not truncated' => [[ + 'path' => self::MOCK_FILE_PATH, + 'expected' => [ [ 'path' => '/path/to/mock/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'dir', 'size' => 57, 'name' => '/path/to/mock/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/fileFoo', 'basename' => '/path/to/mock/fileFoo', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/fileFoo', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/file/Bar', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/file/Bar', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ] ], - true, - false - ], - 'Filepath, not recursive, truncated' => [ - self::MOCK_FILE_PATH, - [ + 'recursive' => true, + 'truncated' => false, + ]], + 'Filepath, not recursive, truncated' => [[ + 'path' => self::MOCK_FILE_PATH, + 'expected' => [ + [ + 'path' => '/path/to/mock/file', + 'mode' => '100644', + 'type' => 'dir', + 'size' => 57, + 'name' => '/path/to/mock/file', + 'contents' => false, + 'stream' => false, + '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' => false, + 'stream' => false, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/file/Bar', + 'name' => '/path/to/mock/file/Bar', + 'mode' => '100644', + 'type' => 'file', + 'size' => 57, + 'visibility' => 'public', + 'contents' => false, + 'stream' => false, + 'timestamp' => null + ], + ], + 'recursive' => false, + 'truncated' => true, + ]], + 'Filepath, recursive, truncated' => [[ + 'path' => self::MOCK_FILE_PATH, + 'expected' => [ [ 'path' => '/path/to/mock/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'dir', 'size' => 57, 'name' => '/path/to/mock/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/fileFoo', 'basename' => '/path/to/mock/fileFoo', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/fileFoo', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, + 'timestamp' => null, + 'visibility' => 'public' + ], + [ + 'path' => '/path/to/mock/file/Bar', + 'mode' => '100644', + 'type' => 'file', + 'size' => 57, + 'name' => '/path/to/mock/file/Bar', + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ] ], - false, - true - ], - 'No Filepath, recursive, not truncated' => [ - '', - [ + 'recursive' => true, + 'truncated' => true, + ]], + 'No Filepath, recursive, not truncated' => [[ + 'path' => '', + 'expected' => [ [ 'path' => '/path/to/mock/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'dir', 'size' => 57, 'name' => '/path/to/mock/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/fileFoo', 'basename' => '/path/to/mock/fileFoo', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/fileFoo', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/file/Bar', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/file/Bar', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => 'some/other/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 747, 'name' => 'some/other/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ] ], - true, - false - ], - 'No Filepath, recursive, truncated' => [ - '', - [ + 'recursive' => true, + 'truncated' => false, + ]], + 'No Filepath, recursive, truncated' => [[ + 'path' => '', + 'expected' => [ [ 'path' => '/path/to/mock/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'dir', 'size' => 57, 'name' => '/path/to/mock/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/fileFoo', 'basename' => '/path/to/mock/fileFoo', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/fileFoo', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => '/path/to/mock/file/Bar', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 57, 'name' => '/path/to/mock/file/Bar', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, 'timestamp' => null, 'visibility' => 'public' ], [ 'path' => 'some/other/file', - 'mode' => 100644, + 'mode' => '100644', 'type' => 'file', 'size' => 747, 'name' => 'some/other/file', - 'contents' => null, - 'stream' => null, + 'contents' => false, + 'stream' => false, '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 - ], + 'recursive' => true, + 'truncated' => true, + ]], ]; } } diff --git a/tests/unit-tests/GithubAdapterTest.php b/tests/unit-tests/GithubAdapterTest.php new file mode 100644 index 0000000..afd1d72 --- /dev/null +++ b/tests/unit-tests/GithubAdapterTest.php @@ -0,0 +1,92 @@ + + * @covers ::__construct + * @covers ::getApi + */ +class GithubAdapterTest extends \PHPUnit_Framework_TestCase +{ + ////////////////////////////////// FIXTURES \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + const MOCK_FILE_PATH = '/path/to/mock/file'; + const MOCK_FOLDER_PATH = 'a-directory'; + + /** @var GithubAdapter */ + private $adapter; + /** @var ApiInterface|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 + * @param mixed $returnValue + */ + final public function testAdapterShouldPassParameterToClient($method, $apiMethod, $parameters, $returnValue = null) + { + if (is_string($returnValue) && is_file(sprintf('%s/../fixtures/%s.json', __DIR__, $returnValue))) { + $fixturePath = sprintf('%s/../fixtures/%s.json', __DIR__, $returnValue); + $fixture = json_decode(file_get_contents($fixturePath), true); + $returnValue = $fixture['tree']; + } + + + $mocker = $this->mockClient->expects(self::exactly(1)) + ->method($apiMethod) + ->willReturn($returnValue) + ; + + $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' => ['has', 'exists', [self::MOCK_FILE_PATH]], + 'read' => ['read', 'getFileContents', [self::MOCK_FILE_PATH]], + 'listContents - File' => ['listContents', 'getTreeMetadata', [self::MOCK_FILE_PATH, false]], + 'listContents - File - recursive' => ['listContents', 'getTreeMetadata', [self::MOCK_FILE_PATH, true]], + 'listContents - Folder' => ['listContents', 'getTreeMetadata', [self::MOCK_FOLDER_PATH, false], ''], + 'listContents - Folder - recursive' => ['listContents', 'getTreeMetadata', [self::MOCK_FOLDER_PATH, true], 'listContents-folder-recursive'], + 'getMetadata' => ['getMetadata', 'getMetadata', [self::MOCK_FILE_PATH]], + 'getSize' => ['getSize', 'getMetadata', [self::MOCK_FILE_PATH]], + 'getMimetype' => ['getMimetype', 'guessMimeType', [self::MOCK_FILE_PATH]], + 'getTimestamp' => ['getTimestamp', 'getLastUpdatedTimestamp', [self::MOCK_FILE_PATH]], + 'getVisibility' => ['getVisibility', 'getTreeMetadata', [self::MOCK_FILE_PATH]], + ]; + } +} diff --git a/tests/SettingsTest.php b/tests/unit-tests/SettingsTest.php similarity index 99% rename from tests/SettingsTest.php rename to tests/unit-tests/SettingsTest.php index bd46a24..305fd21 100644 --- a/tests/SettingsTest.php +++ b/tests/unit-tests/SettingsTest.php @@ -226,6 +226,8 @@ final public function provideInvalidRepositoryNames() [array()], ['foo'], ['/foo'], + ['foo/'], + ['foo//bar'], ['foo/bar/'], ['/foo/bar/'], ['foo/bar/baz'],