diff --git a/app/Phabricator/ProjectsMap.php b/app/Phabricator/ProjectsMap.php index aeaf39a..add5015 100644 --- a/app/Phabricator/ProjectsMap.php +++ b/app/Phabricator/ProjectsMap.php @@ -1,278 +1,278 @@ <?php namespace Nasqueron\Notifications\Phabricator; -use Nasqueron\Notifications\Phabricator\PhabricatorAPIClient as ApiClient; +use Nasqueron\Notifications\Contracts\APIClient as APIClient; use App; use Cache; class ProjectsMap implements \IteratorAggregate, \ArrayAccess { /// /// Private properties and constants /// /** * The maximum number of projects to fetch */ const LIMIT = 1000; /** * The projects as an array with phid as keys, project names as $value * * @var string[] */ private $map = []; /** * The Phabricator instance name for this projects map * * @var string */ private $instanceName; /** * * @var Nasqueron\Notifications\Contracts\APIClient */ private $apiClient; /** * The source of the map * * @var string */ private $source = 'unloaded'; /// /// Constructor /// /** * Initializes a new instance of ProjectsMap. * * @param string $instanceName The Phabricator instance name */ public function __construct ($instanceName) { $this->instanceName = $instanceName; } /// /// IteratorAggregate interface implementation /// /** * Gets iterator. * * @return Traversable */ public function getIterator () { return new \ArrayIterator($this->map); } /// /// ArrayAccess interface implementation /// /** * Determines whether an offset exists. * * @param mixed $offset The offset * @return bool */ public function offsetExists ($offset) { return array_key_exists($offset, $this->map); } /** * Gets the value at the specified offset. * * @param mixed $offset The offset. * @return mixed The value */ public function offsetGet ($offset) { return $this->map[$offset]; } /** * Assigns a value to the specified offset. * * @param mixed $offset The offset * @param mixed $value The value to assign */ public function offsetSet ($offset, $value) { $this->map[$offset] = $value; } /** * Unsets a value at the specified offset. * * @param mixed $offset The offset where to remove the value */ public function offsetUnset ($offset) { unset($this->map[$offset]); } /// /// Static constructors /// /** * Gets a new ProjectsMap instance from cache or API when not cached. * * @param string $phabricatorInstanceName The Phabricator instance name * @return ProjectsMap */ public static function load ($phabricatorInstanceName) { $instance = new self($phabricatorInstanceName); if ($instance->isCached()) { $instance->loadFromCache(); } else { $instance->fetchFromAPI(); } return $instance; } /** * Gets a new ProjectsMap instance and queries Phabricator API to fill it. * * @param string $phabricatorInstanceName The Phabricator instance name * @param Nasqueron\Notifications\Contracts\APIClient $apiClient The Phabricator API client * @return ProjectsMap */ - public static function fetch ($phabricatorInstanceName, APIClient $apiClient = null) { + public static function fetch ($phabricatorInstanceName, ?APIClient $apiClient = null) { $instance = new self($phabricatorInstanceName); $instance->setAPIClient($apiClient); $instance->fetchFromAPI(); return $instance; } /// /// API /// /** * @return Nasqueron\Notifications\Contracts\APIClient */ public function getAPIClient () { if ($this->apiClient === null) { $factory = App::make('phabricator-api'); $this->apiClient = $factory->getForProject($this->instanceName); } return $this->apiClient; } /** * @param Nasqueron\Notifications\Contracts\APIClient $apiClient */ - public function setAPIClient (APIClient $apiClient = null) { + public function setAPIClient (?APIClient $apiClient = null) { $this->apiClient = $apiClient; } /** * Fetches the projects' map from the Phabricator API. * * @throws \Exception when API reply is empty or invalid. */ private function fetchFromAPI () { $reply = $this->getAPIClient()->call( 'project.query', [ 'limit' => self::LIMIT ] ); if (!$reply) { throw new \Exception("Empty reply calling project.query at $this->instanceName Conduit API."); } if (!property_exists($reply, 'data')) { throw new \Exception("Invalid reply calling project.query at $this->instanceName Conduit API."); } foreach ($reply->data as $phid => $projectInfo) { $this->offsetSet($phid, $projectInfo->name); } $this->source = 'api'; } /// /// Cache /// /** * Gets cache key. * * @return string The cache key for the current projects map */ private function getCacheKey () { return class_basename(get_class($this)) . '-' . md5($this->instanceName); } /** * Determines if the instance is cached * * @return bool true if cached; otherwise, false. */ public function isCached () { return Cache::has($this->getCacheKey()); } /** * Saves data to cache */ public function saveToCache () { Cache::forever($this->getCacheKey(), $this->map); } /** * Loads data from cache * * Populates 'map' and 'source' properties */ public function loadFromCache () { $cachedMap = Cache::get($this->getCacheKey()); if ($cachedMap !== null) { $this->map = $cachedMap; $this->source = 'cache'; } } /// /// Output /// /** * Gets project name, refreshing the cache if needed. * * @param string $projectPHID the PHID of the project to query the name * @return string The name of the poject, or an empty string if not found */ public function getProjectName ($projectPHID) { if ($this->offsetExists($projectPHID)) { return $this->offsetGet($projectPHID); } if ($this->source !== 'api') { $this->fetchFromAPI(); return $this->getProjectName($projectPHID); } return ""; } /** * Returns the projects map as an array. * * @return array An array, each row containing ['PHID', 'project name'] */ public function toArray () { $array = []; foreach ($this->map as $phid => $projectName) { $array[] = [$phid, $projectName]; } return $array; } } diff --git a/composer.json b/composer.json index d58dfb3..a30893d 100644 --- a/composer.json +++ b/composer.json @@ -1,56 +1,56 @@ { "name": "nasqueron/notifications", "description": "Nasqueron notifications center", "keywords": [ "nasqueron", "activemq", "AMQP", "notifications" ], "license": "BSD-2-Clause", "type": "project", "require": { - "php": ">=5.6.0", + "php": ">=7.1.0", "laravel/framework": "5.2.*", "guzzlehttp/guzzle": "^6.2", "keruald/dockerhub": "^0.0.3", "keruald/github": "^0.2.0", "keruald/broker": "^0.4.1", "keruald/mailgun": "^0.0.1", "netresearch/jsonmapper": "~0.1.0", "sentry/sentry": "^0.13.0" }, "require-dev": { "fzaninotto/faker": "~1.4", "mockery/mockery": "0.9.*", "phpmd/phpmd" : "@stable", "phpunit/phpunit": "~4.0", "phpspec/phpspec": "~2.1", "squizlabs/php_codesniffer": "2.*", "symfony/css-selector": "~3.0", "symfony/dom-crawler": "~3.0" }, "autoload": { "psr-4": { "Nasqueron\\Notifications\\": "app/", "Nasqueron\\Notifications\\Tests\\": "tests/" } }, "scripts": { "post-root-package-install": [ "php -r \"copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "php artisan key:generate" ], "phpmd": [ "vendor/bin/phpmd app/ xml ruleset.xml" ], "test": [ "phpunit --no-coverage" ] }, "config": { "preferred-install": "dist" } } diff --git a/tests/Phabricator/ProjectsMapTest.php b/tests/Phabricator/ProjectsMapTest.php index dd2e5b2..273e6e2 100644 --- a/tests/Phabricator/ProjectsMapTest.php +++ b/tests/Phabricator/ProjectsMapTest.php @@ -1,192 +1,203 @@ <?php namespace Nasqueron\Notifications\Tests\Phabricator; +use Nasqueron\Notifications\Contracts\APIClient; use Nasqueron\Notifications\Phabricator\ProjectsMap; use Nasqueron\Notifications\Tests\TestCase; use Mockery; class ProjectsMapTest extends TestCase { /** * @var Nasqueron\Notifications\Phabricator\ProjectsMap */ private $map; public function setUp () { parent::setUp(); // // We mock the API, so an imaginary instance of Phabricator // will return 3 results: Accounts, Agora & architecture. // // Agora has the key "PHID-PROJ-cztcgpvqr6smnnekotq7". // $this->mockPhabricatorAPIForProjectsMap(); $this->map = ProjectsMap::fetch("http://phabricator.acme.tld"); } public function testIteratorIsTraversable () { $this->assertInstanceOf( "Traversable", $this->map->getIterator() ); } /// /// Tests for ArrayAccess /// public function testOffsetExistsWhenItDoes () { $this->assertTrue( $this->map->offsetExists("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } public function testOffsetExistsWhenItDoesNot () { $this->assertFalse( $this->map->offsetExists("non-existing-key") ); } public function testOffsetGetWhenItDoesExist () { $this->assertSame( "Agora", $this->map->offsetGet("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } /** * @expectedException ErrorException */ public function testOffsetGetWhenItDoesNotExist () { $this->map->offsetGet("non-existing-key"); } /** * @covers Nasqueron\Notifications\Phabricator\ProjectsMap::offsetSet */ public function testOffsetSet () { $this->map->offsetSet("newkey", "quux"); $this->assertSame("quux", $this->map->offsetGet("newkey")); } /** * @covers Nasqueron\Notifications\Phabricator\ProjectsMap::offsetUnset */ public function testOffsetUnset () { unset($this->map["PHID-PROJ-cztcgpvqr6smnnekotq7"]); $this->assertFalse( $this->map->offsetExists("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } /// /// Tests for cache /// public function testCache () { $this->assertFalse($this->map->isCached()); $this->map->saveToCache(); $this->assertTrue($this->map->isCached()); } public function testLoadFromCache () { $this->map->saveToCache(); $map = new ProjectsMap("http://phabricator.acme.tld"); $map->loadFromCache(); $this->assertTrue( $map->offsetExists("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } public function testLoadWhenInCache () { $this->map->saveToCache(); $map = ProjectsMap::load("http://phabricator.acme.tld"); $this->assertTrue( $map->offsetExists("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } /// /// Tests for helper methods /// public function testGetProjectName () { $this->assertSame( "Agora", $this->map->getProjectName("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } public function testGetProjectNameForNewInstance () { $map = new ProjectsMap("http://phabricator.acme.tld"); $this->assertSame( "Agora", $map->getProjectName("PHID-PROJ-cztcgpvqr6smnnekotq7") ); } public function testGetProjectNameWhenItDoesNotExist () { $this->assertSame( "", $this->map->getProjectName("non-existing-key") ); } public function testToArrayProducesArray () { $array = $this->map->toArray(); $this->assertTrue( is_array($array), "Test if toArray return an array" ); } public function testThatArrayCount () { $array = $this->map->toArray(); $this->assertSame(3, count($array)); } public function testThatArrayContainsExpectedData () { $this->assertSame( [ ["PHID-PROJ-6dg6ogx5pjmk24ur4tp4", "Accounts"], ["PHID-PROJ-cztcgpvqr6smnnekotq7", "Agora"], ["PHID-PROJ-3iew3cqf3htpazfyzb5a", "architecture"] ], $this->map->toArray() ); } /// /// Tests API /// - private function mockPhabricatorAPIWithReply ($reply) { - return Mockery::mock('Nasqueron\Notifications\Contract\APIClient') - ->shouldReceive('call') - ->andReturn($reply); + private function mockPhabricatorAPIWithReply ($reply) : APIClient { + return (new class($reply) implements APIClient { + private $reply; + + public function __construct ($reply) { + $this->reply = $reply; + } + + public function setEndPoint ($url) : void { } + + public function call ($method, $arguments = []) { + return $this->reply; + } + }); } /** * @expectedException Exception */ public function testFetchFromAPIWithoutReply () { $mock = $this->mockPhabricatorAPIWithReply(false); ProjectsMap::fetch("http://phabricator.acme.tld", $mock); } /** * @expectedException Exception */ public function testFetchFromAPIInvalidReply () { $mock = $this->mockPhabricatorAPIWithReply(new \stdClass); ProjectsMap::fetch("http://phabricator.acme.tld", $mock); } }