From 87b777a80faac538eeadef9ccaec75a3886ae460 Mon Sep 17 00:00:00 2001 From: Jamie Hannaford Date: Wed, 30 Mar 2016 18:00:00 +0200 Subject: [PATCH 1/4] Add better integration tests --- tests/integration/Runner.php | 129 ++++++++---- tests/integration/SampleManager.php | 104 --------- tests/integration/TestCase.php | 21 +- ...OperatorTest.php => OperatorTraitTest.php} | 57 ++--- ...tTes.php => HydratorStrategyTraitTest.php} | 10 + .../Common/Resource/AbstractResourceTest.php | 98 ++------- .../Common/Resource/OperatorResourceTest.php | 198 ++++++++++++++++++ tests/unit/Common/Service/BuilderTest.php | 120 +++++------ tests/unit/Common/Service/Fixtures/Api.php | 9 + .../Common/Service/Fixtures/Identity/Api.php | 9 + .../Service/Fixtures/Identity/Service.php | 9 + .../Common/Service/Fixtures/Models/Foo.php | 13 ++ .../unit/Common/Service/Fixtures/Service.php | 9 + .../Common/Transport/JsonSerializerTest.php | 108 +++++++++- .../Transport/RequestSerializerTest.php | 31 +++ 15 files changed, 594 insertions(+), 331 deletions(-) delete mode 100644 tests/integration/SampleManager.php rename tests/unit/Common/Api/{OperatorTest.php => OperatorTraitTest.php} (82%) rename tests/unit/Common/{HydratorStrategyTraitTes.php => HydratorStrategyTraitTest.php} (83%) create mode 100644 tests/unit/Common/Resource/OperatorResourceTest.php create mode 100644 tests/unit/Common/Service/Fixtures/Api.php create mode 100644 tests/unit/Common/Service/Fixtures/Identity/Api.php create mode 100644 tests/unit/Common/Service/Fixtures/Identity/Service.php create mode 100644 tests/unit/Common/Service/Fixtures/Models/Foo.php create mode 100644 tests/unit/Common/Service/Fixtures/Service.php diff --git a/tests/integration/Runner.php b/tests/integration/Runner.php index b1003a2..9b594da 100644 --- a/tests/integration/Runner.php +++ b/tests/integration/Runner.php @@ -4,31 +4,42 @@ class Runner { - private $basePath; + private $testsDir; + private $samplesDir; private $logger; - private $services = []; + private $tests; private $namespace; - public function __construct($basePath, $testNamespace) + public function __construct($samplesDir, $testsDir, $testNamespace) { - $this->basePath = $basePath; + $this->samplesDir = $samplesDir; + $this->testsDir = $testsDir; $this->namespace = $testNamespace; $this->logger = new DefaultLogger(); - $this->assembleServicesFromSamples(); + $this->assembleTestFiles(); } - private function traverse($path) + private function traverse(string $path): \DirectoryIterator { - return new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS); + return new \DirectoryIterator($path); } - private function assembleServicesFromSamples() + private function assembleTestFiles() { - foreach ($this->traverse($this->basePath) as $servicePath) { + foreach ($this->traverse($this->testsDir) as $servicePath) { if ($servicePath->isDir()) { - foreach ($this->traverse($servicePath) as $versionPath) { - $this->services[$servicePath->getBasename()][] = $versionPath->getBasename(); + $serviceBn = $servicePath->getBasename(); + foreach ($this->traverse($servicePath->getPathname()) as $versionPath) { + $versionBn = $versionPath->getBasename(); + if ($servicePath->isDir() && $versionBn[0] == 'v') { + foreach ($this->traverse($versionPath->getPathname()) as $testPath) { + if (strpos($testPath->getFilename(), 'Test.php')) { + $testBn = strtolower(substr($testPath->getBasename(), 0, -8)); + $this->tests[strtolower($serviceBn)][strtolower($versionBn)][] = $testBn; + } + } + } } } } @@ -36,55 +47,77 @@ private function assembleServicesFromSamples() private function getOpts() { - $opts = getopt('s:v:t:', ['service:', 'version:', 'test::', 'debug::', 'help::']); + $opts = getopt('s:v:m:t:', ['service:', 'version:', 'module::', 'test::', 'debug::', 'help::']); $getOpt = function (array $keys, $default) use ($opts) { + $value = $default; foreach ($keys as $key) { if (isset($opts[$key])) { - return $opts[$key]; + $value = $opts[$key]; + break; } } - return $default; + return strtolower($value); }; return [ $getOpt(['s', 'service'], 'all'), - $getOpt(['n', 'version'], 'all'), + $getOpt(['v', 'version'], 'all'), + $getOpt(['m', 'module'], 'core'), $getOpt(['t', 'test'], ''), - isset($opts['debug']) ? (int) $opts['debug'] : 0, + isset($opts['debug']) ? (int)$opts['debug'] : 0, ]; } - private function getRunnableServices($service, $version) + private function getRunnableServices($service, $version, $module) { - $services = $this->services; + $tests = $this->tests; if ($service != 'all') { - if (!isset($this->services[$service])) { - throw new \InvalidArgumentException(sprintf("%s service does not exist", $service)); + if (!isset($tests[$service])) { + $this->logger->critical(sprintf("%s is not a valid service", $service)); + exit(1); } - $versions = ($version == 'all') ? $this->services[$service] : [$version]; - $services = [$service => $versions]; + $serviceArray = $tests[$service]; + $tests = [$service => $serviceArray]; + + if ($version != 'all') { + if (!isset($serviceArray[$version])) { + $this->logger->critical(sprintf("%s is not a valid version for the %s service", $version, $service)); + exit(1); + } + + $versionArray = $serviceArray[$version]; + if ($module != 'core') { + if (!in_array($module, $serviceArray[$version])) { + $this->logger->critical(sprintf("%s is not a valid test class for the %s %s service", $module, $version, $service)); + exit(1); + } + $versionArray = [$module]; + } + + $tests = [$service => [$version => $versionArray]]; + } } - return $services; + return $tests; } /** * @return TestInterface */ - private function getTest($serviceName, $version, $verbosity) + private function getTest($service, $version, $test, $verbosity) { - $className = sprintf("%s\\%s\\%sTest", $this->namespace, Utils::toCamelCase($serviceName), ucfirst($version)); + $className = sprintf("%s\\%s\\%s\\%sTest", $this->namespace, Utils::toCamelCase($service), $version, ucfirst($test)); if (!class_exists($className)) { throw new \RuntimeException(sprintf("%s does not exist", $className)); } - $basePath = $this->basePath . DIRECTORY_SEPARATOR . $serviceName . DIRECTORY_SEPARATOR . $version; - $smClass = sprintf("%s\\SampleManager", $this->namespace); - $class = new $className($this->logger, new $smClass($basePath, $verbosity)); + $basePath = $this->samplesDir . DIRECTORY_SEPARATOR . $service . DIRECTORY_SEPARATOR . $version; + $smClass = sprintf("%s\\SampleManager", $this->namespace); + $class = new $className($this->logger, new $smClass($basePath, $verbosity), $verbosity); if (!($class instanceof TestInterface)) { throw new \RuntimeException(sprintf("%s does not implement TestInterface", $className)); @@ -95,19 +128,35 @@ private function getTest($serviceName, $version, $verbosity) public function runServices() { - list($serviceOpt, $versionOpt, $testMethodOpt, $verbosityOpt) = $this->getOpts(); - - foreach ($this->getRunnableServices($serviceOpt, $versionOpt) as $serviceName => $versions) { - foreach ($versions as $version) { - $testRunner = $this->getTest($serviceName, $version, $verbosityOpt); - - if ($testMethodOpt) { - $testRunner->runOneTest($testMethodOpt); - } else { - $testRunner->runTests(); + list ($serviceOpt, $versionOpt, $moduleOpt, $testMethodOpt, $verbosityOpt) = $this->getOpts(); + + foreach ($this->getRunnableServices($serviceOpt, $versionOpt, $moduleOpt) as $serviceName => $serviceArray) { + foreach ($serviceArray as $versionName => $versionArray) { + foreach ($versionArray as $testName) { + + $this->logger->info(str_repeat('=', 49)); + $this->logger->info("Starting %s %v %m integration test(s)", [ + '%s' => $serviceName, + '%v' => $versionName, + '%m' => $moduleOpt, + ]); + $this->logger->info(str_repeat('=', 49)); + + $testRunner = $this->getTest($serviceName, $versionName, $testName, $verbosityOpt); + + try { + if ($testMethodOpt) { + $testRunner->runOneTest($testMethodOpt); + } else { + $testRunner->runTests(); + } + } finally { + $this->logger->info(str_repeat('=', 11)); + $this->logger->info('Cleaning up'); + $this->logger->info(str_repeat('=', 11)); + $testRunner->teardown(); + } } - - $testRunner->teardown(); } } } diff --git a/tests/integration/SampleManager.php b/tests/integration/SampleManager.php deleted file mode 100644 index 4bd8b9a..0000000 --- a/tests/integration/SampleManager.php +++ /dev/null @@ -1,104 +0,0 @@ -basePath = $basePath; - $this->verbosity = $verbosity; - } - - public function deletePaths() - { - if (!empty($this->paths)) { - foreach ($this->paths as $path) { - unlink($path); - } - } - } - - protected function getGlobalReplacements() - { - return [ - '{userId}' => getenv('OS_USER_ID'), - '{username}' => getenv('OS_USERNAME'), - '{password}' => getenv('OS_PASSWORD'), - '{domainId}' => getenv('OS_DOMAIN_ID'), - '{authUrl}' => getenv('OS_AUTH_URL'), - '{tenantId}' => getenv('OS_TENANT_ID'), - '{region}' => getenv('OS_REGION'), - '{projectId}' => getenv('OS_PROJECT_ID'), - '{projectName}' => getenv('OS_PROJECT_NAME'), - ]; - } - - protected function getConnectionTemplate() - { - if ($this->verbosity === 1) { - $subst = <<<'EOL' -use OpenCloud\Integration\DefaultLogger; -use OpenCloud\Integration\Utils; -use GuzzleHttp\MessageFormatter; - -$options = [ - 'debugLog' => true, - 'logger' => new DefaultLogger(), - 'messageFormatter' => new MessageFormatter(), -]; -$openstack = new OpenCloud\OpenCloud(Utils::getAuthOpts($options)); -EOL; - } elseif ($this->verbosity === 2) { - $subst = <<<'EOL' -use OpenCloud\Integration\DefaultLogger; -use OpenCloud\Integration\Utils; -use GuzzleHttp\MessageFormatter; - -$options = [ - 'debugLog' => true, - 'logger' => new DefaultLogger(), - 'messageFormatter' => new MessageFormatter(MessageFormatter::DEBUG), -]; -$openstack = new OpenCloud\OpenCloud(Utils::getAuthOpts($options)); -EOL; - } else { - $subst = <<<'EOL' -use OpenCloud\Integration\Utils; - -$openstack = new OpenCloud\OpenCloud(Utils::getAuthOpts()); -EOL; - } - - return $subst; - } - - public function write($path, array $replacements) - { - $replacements = array_merge($this->getGlobalReplacements(), $replacements); - - $sampleFile = rtrim($this->basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $path; - - if (!file_exists($sampleFile) || !is_readable($sampleFile)) { - throw new \RuntimeException(sprintf("%s either does not exist or is not readable", $sampleFile)); - } - - $content = strtr(file_get_contents($sampleFile), $replacements); - $content = str_replace("'vendor/'", "'" . dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . "vendor'", $content); - - $subst = $this->getConnectionTemplate(); - $content = preg_replace('/\([^)]+\)/', '', $content, 1); - $content = str_replace('$openstack = new OpenCloud\OpenCloud;', $subst, $content); - - $tmp = tempnam(sys_get_temp_dir(), 'openstack'); - file_put_contents($tmp, $content); - - $this->paths[] = $tmp; - - return $tmp; - } -} diff --git a/tests/integration/TestCase.php b/tests/integration/TestCase.php index cb81381..5c73ff8 100644 --- a/tests/integration/TestCase.php +++ b/tests/integration/TestCase.php @@ -2,6 +2,7 @@ namespace OpenCloud\Integration; +use OpenCloud\Common\Resource\Deletable; use Psr\Log\LoggerInterface; abstract class TestCase extends \PHPUnit_Framework_TestCase implements TestInterface @@ -10,6 +11,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase implements TestInter private $startPoint; private $lastPoint; private $sampleManager; + private $namePrefix = 'phptest_'; public function __construct(LoggerInterface $logger, SampleManagerInterface $sampleManager) { @@ -75,7 +77,7 @@ protected function randomStr($length = 5) $randomString .= $chars[rand(0, $charsLen - 1)]; } - return 'phptest_' . $randomString; + return $this->namePrefix . $randomString; } private function formatMinDifference($duration) @@ -112,4 +114,21 @@ protected function sampleFile(array $replacements, $path) { return $this->sampleManager->write($path, $replacements); } + + protected function getBaseClient() + { + return eval($this->sampleManager->getConnectionStr()); + } + + protected function deleteItems(\Generator $items) + { + foreach ($items as $item) { + if ($item instanceof Deletable + && property_exists($item, 'name') + && strpos($item->name, $this->namePrefix) === 0 + ) { + $item->delete(); + } + } + } } diff --git a/tests/unit/Common/Api/OperatorTest.php b/tests/unit/Common/Api/OperatorTraitTest.php similarity index 82% rename from tests/unit/Common/Api/OperatorTest.php rename to tests/unit/Common/Api/OperatorTraitTest.php index e22c25e..fe07eab 100644 --- a/tests/unit/Common/Api/OperatorTest.php +++ b/tests/unit/Common/Api/OperatorTraitTest.php @@ -4,19 +4,19 @@ use function GuzzleHttp\Psr7\uri_for; use GuzzleHttp\Promise\Promise; -use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Psr7\Uri; -use OpenCloud\Common\Api\Operator; +use OpenCloud\Common\Api\OperatorTrait; use OpenCloud\Common\Resource\AbstractResource; use OpenCloud\Common\Resource\ResourceInterface; use OpenCloud\Test\Fixtures\ComputeV2Api; use OpenCloud\Test\TestCase; use Prophecy\Argument; -class OperatorTest extends TestCase +class OperatorTraitTest extends TestCase { + /** @var TestOperator */ private $operator; + private $def; public function setUp() @@ -56,22 +56,6 @@ public function test_it_sends_a_request_when_async_operations_are_executed() $this->operator->executeAsync($this->def, []); } - public function test_it_returns_a_model_instance() - { - $this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class)); - } - - public function test_it_populates_models_from_response() - { - $this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, new Response(200))); - } - - public function test_it_populates_models_from_arrays() - { - $data = ['flavor' => [], 'image' => []]; - $this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, $data)); - } - public function test_it_wraps_sequential_ops_in_promise_when_async_is_appended_to_method_name() { $promise = $this->operator->createAsync('something'); @@ -93,18 +77,6 @@ public function test_it_throws_exception_when_async_is_called_on_a_non_existent_ $this->operator->fooAsync(); } - public function test_it_retrieves_base_http_url() - { - $returnedUri = uri_for('http://foo.com'); - - $this->client->getConfig('base_uri')->shouldBeCalled()->willReturn($returnedUri); - - $uri = $this->operator->testBaseUri(); - - $this->assertInstanceOf(Uri::class, $uri); - $this->assertEquals($returnedUri, $uri); - } - /** * @expectedException \Exception */ @@ -112,18 +84,29 @@ public function test_undefined_methods_result_in_error() { $this->operator->foo(); } + + public function test_it_returns_a_model_instance() + { + $this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class)); + } + public function test_it_populates_models_from_response() + { + $this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, new Response(200))); + } + public function test_it_populates_models_from_arrays() + { + $data = ['flavor' => [], 'image' => []]; + $this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, $data)); + } } class TestResource extends AbstractResource { } -class TestOperator extends Operator +class TestOperator { - public function testBaseUri() - { - return $this->getHttpBaseUrl(); - } + use OperatorTrait; public function create($str) { diff --git a/tests/unit/Common/HydratorStrategyTraitTes.php b/tests/unit/Common/HydratorStrategyTraitTest.php similarity index 83% rename from tests/unit/Common/HydratorStrategyTraitTes.php rename to tests/unit/Common/HydratorStrategyTraitTest.php index 6841002..61a4886 100644 --- a/tests/unit/Common/HydratorStrategyTraitTes.php +++ b/tests/unit/Common/HydratorStrategyTraitTest.php @@ -7,6 +7,7 @@ class HydratorStrategyTraitTest extends TestCase { + /** @var Fixture */ private $fixture; public function setUp() @@ -31,6 +32,14 @@ public function test_it_hydrates_aliases() $this->assertEquals(1, $this->fixture->foo); } + + public function test_it_sets() + { + $data = ['foo1' => 1]; + + $this->fixture->set('foo1', 'foo', $data); + $this->assertEquals(1, $this->fixture->foo); + } } class Fixture @@ -45,6 +54,7 @@ public function getBar() { return $this->bar; } + public function getBaz() { return $this->baz; diff --git a/tests/unit/Common/Resource/AbstractResourceTest.php b/tests/unit/Common/Resource/AbstractResourceTest.php index f334292..2255077 100644 --- a/tests/unit/Common/Resource/AbstractResourceTest.php +++ b/tests/unit/Common/Resource/AbstractResourceTest.php @@ -5,13 +5,13 @@ use function GuzzleHttp\Psr7\stream_for; use GuzzleHttp\Psr7\Response; use OpenCloud\Common\Resource\AbstractResource; -use OpenCloud\Common\Resource\Generator; -use OpenCloud\Test\Fixtures\ComputeV2Api; +use OpenCloud\Common\Resource\ResourceInterface; use OpenCloud\Test\TestCase; use Prophecy\Argument; class AbstractResourceTest extends TestCase { + /** @var TestResource */ private $resource; public function setUp() @@ -19,7 +19,8 @@ public function setUp() parent::setUp(); $this->rootFixturesDir = __DIR__; - $this->resource = new TestResource($this->client->reveal(), new ComputeV2Api()); + + $this->resource = new TestResource(); } public function test_it_populates_from_response() @@ -42,6 +43,14 @@ public function test_it_populates_datetimes_from_arrays() $this->assertEquals($this->resource->created, $dt); } + public function test_it_populates_model_objects_from_arrays() + { + $tr = new TestResource(); + $this->resource->populateFromArray(['child' => $tr]); + + $this->assertEquals($this->resource->child, $tr); + } + public function test_it_populates_arrays_from_arrays() { $this->resource->populateFromArray(['children' => [$this->resource, $this->resource]]); @@ -56,92 +65,26 @@ public function test_it_gets_attrs() $this->assertEquals(['bar' => 'foo'], $this->resource->getAttrs(['bar'])); } - public function test_it_executes_with_state() + public function test_it_returns_a_model_instance() { - $this->resource->id = 'foo'; - $this->resource->bar = 'bar'; - - $expectedJson = ['id' => 'foo', 'bar' => 'bar']; - - $this->setupMock('GET', 'foo', $expectedJson, [], new Response(204)); - - $this->resource->executeWithState((new ComputeV2Api())->test()); + $this->assertInstanceOf(ResourceInterface::class, $this->resource->model(TestResource::class)); } - public function test_it_executes_operations_until_a_204_is_received() + public function test_it_populates_models_from_response() { - $this->client - ->request('GET', 'servers', ['headers' => []]) - ->shouldBeCalled() - ->willReturn($this->getFixture('servers-page1')); - - $this->client - ->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []]) - ->shouldBeCalled() - ->willReturn(new Response(204)); - - $count = 0; - - $api = new ComputeV2Api(); - - foreach ($this->resource->enumerate($api->getServers()) as $item) { - $count++; - $this->assertInstanceOf(TestResource::class, $item); - } - - $this->assertEquals(5, $count); + $this->assertInstanceOf(ResourceInterface::class, $this->resource->model(TestResource::class, new Response(200))); } - public function test_it_invokes_function_if_provided() + public function test_it_populates_models_from_arrays() { - $this->client - ->request('GET', 'servers', ['headers' => []]) - ->shouldBeCalled() - ->willReturn($this->getFixture('servers-page1')); - - $this->client - ->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []]) - ->shouldBeCalled() - ->willReturn(new Response(204)); - - $api = new ComputeV2Api(); - - $count = 0; - - $fn = function () use (&$count) { - $count++; - }; - - foreach ($this->resource->enumerate($api->getServers(), [], $fn) as $item) { - } - - $this->assertEquals(5, $count); - } - - public function test_it_halts_when_user_provided_limit_is_reached() - { - $this->client - ->request('GET', 'servers', ['query' => ['limit' => 2], 'headers' => []]) - ->shouldBeCalled() - ->willReturn($this->getFixture('servers-page1')); - - $count = 0; - - $api = new ComputeV2Api(); - - foreach ($this->resource->enumerate($api->getServers(), ['limit' => 2]) as $item) { - $count++; - } - - $this->assertEquals(2, $count); + $data = ['flavor' => [], 'image' => []]; + $this->assertInstanceOf(ResourceInterface::class, $this->resource->model(TestResource::class, $data)); } } class TestResource extends AbstractResource { protected $resourceKey = 'foo'; - protected $resourcesKey = 'servers'; - protected $markerKey = 'id'; /** @var string */ public $bar; @@ -154,6 +97,9 @@ class TestResource extends AbstractResource /** @var []TestResource */ public $children; + /** @var TestResource */ + public $child; + public function getAttrs(array $keys) { return parent::getAttrs($keys); diff --git a/tests/unit/Common/Resource/OperatorResourceTest.php b/tests/unit/Common/Resource/OperatorResourceTest.php new file mode 100644 index 0000000..b487982 --- /dev/null +++ b/tests/unit/Common/Resource/OperatorResourceTest.php @@ -0,0 +1,198 @@ +rootFixturesDir = __DIR__; + + $this->resource = new TestOperatorResource($this->client->reveal(), new Api()); + } + + public function test_it_retrieves_base_http_url() + { + $returnedUri = \GuzzleHttp\Psr7\uri_for('http://foo.com'); + $this->client->getConfig('base_uri')->shouldBeCalled()->willReturn($returnedUri); + + $uri = $this->resource->testBaseUri(); + + $this->assertInstanceOf(Uri::class, $uri); + $this->assertEquals($returnedUri, $uri); + } + + public function test_it_executes_with_state() + { + $this->resource->id = 'foo'; + $this->resource->bar = 'bar'; + + $expectedJson = ['id' => 'foo', 'bar' => 'bar']; + + $this->setupMock('GET', 'foo', $expectedJson, [], new Response(204)); + + $this->resource->executeWithState((new ComputeV2Api())->test()); + } + + public function test_it_executes_operations_until_a_204_is_received() + { + $this->client + ->request('GET', 'servers', ['headers' => []]) + ->shouldBeCalled() + ->willReturn($this->getFixture('servers-page1')); + + $this->client + ->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []]) + ->shouldBeCalled() + ->willReturn(new Response(204)); + + $count = 0; + + $api = new ComputeV2Api(); + + foreach ($this->resource->enumerate($api->getServers()) as $item) { + $count++; + $this->assertInstanceOf(TestOperatorResource::class, $item); + } + + $this->assertEquals(5, $count); + } + + public function test_it_invokes_function_if_provided() + { + $this->client + ->request('GET', 'servers', ['headers' => []]) + ->shouldBeCalled() + ->willReturn($this->getFixture('servers-page1')); + + $this->client + ->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []]) + ->shouldBeCalled() + ->willReturn(new Response(204)); + + $api = new ComputeV2Api(); + + $count = 0; + + $fn = function () use (&$count) { + $count++; + }; + + foreach ($this->resource->enumerate($api->getServers(), [], $fn) as $item) { + } + + $this->assertEquals(5, $count); + } + + public function test_it_halts_when_user_provided_limit_is_reached() + { + $this->client + ->request('GET', 'servers', ['query' => ['limit' => 2], 'headers' => []]) + ->shouldBeCalled() + ->willReturn($this->getFixture('servers-page1')); + + $count = 0; + + $api = new ComputeV2Api(); + + foreach ($this->resource->enumerate($api->getServers(), ['limit' => 2]) as $item) { + $count++; + } + + $this->assertEquals(2, $count); + } + + public function test_it_predicts_resources_key_without_explicit_property() + { + $this->client + ->request('GET', 'servers', ['query' => ['limit' => 2], 'headers' => []]) + ->shouldBeCalled() + ->willReturn($this->getFixture('servers-page1')); + + $count = 0; + + $api = new ComputeV2Api(); + $resource = new Server($this->client->reveal(), new $api); + + foreach ($resource->enumerate($api->getServers(), ['limit' => 2]) as $item) { + $count++; + } + + $this->assertEquals(2, $count); + } + + public function test_it_extracts_multiple_instances() + { + $response = $this->getFixture('servers-page1'); + $resource = new Server($this->client->reveal(), new Api()); + + $resources = $resource->extractMultipleInstances($response); + + foreach ($resources as $resource) { + $this->assertInstanceOf(Server::class, $resource); + } + } + + public function test_it_finds_parent_service() + { + $r = new Foo($this->client->reveal(), new Api()); + $this->assertInstanceOf(Service::class, $r->testGetService()); + } + + public function test_it_returns_a_model_instance() + { + $this->assertInstanceOf(ResourceInterface::class, $this->resource->model(TestResource::class)); + } + + public function test_it_populates_models_from_response() + { + $this->assertInstanceOf(ResourceInterface::class, $this->resource->model(TestResource::class, new Response(200))); + } + + public function test_it_populates_models_from_arrays() + { + $data = ['flavor' => [], 'image' => []]; + $this->assertInstanceOf(ResourceInterface::class, $this->resource->model(TestResource::class, $data)); + } +} + +class TestOperatorResource extends OperatorResource +{ + protected $resourceKey = 'foo'; + protected $resourcesKey = 'servers'; + protected $markerKey = 'id'; + + /** @var string */ + public $bar; + + public $id; + + /** @var \DateTimeImmutable */ + public $created; + + /** @var []TestResource */ + public $children; + + public function testBaseUri() + { + return $this->getHttpBaseUrl(); + } +} + +class Server extends OperatorResource +{} \ No newline at end of file diff --git a/tests/unit/Common/Service/BuilderTest.php b/tests/unit/Common/Service/BuilderTest.php index 09e9918..224ccdf 100644 --- a/tests/unit/Common/Service/BuilderTest.php +++ b/tests/unit/Common/Service/BuilderTest.php @@ -2,13 +2,10 @@ namespace OpenCloud\Test\Common\Service; -use GuzzleHttp\ClientInterface; +use OpenCloud\Common\Auth\IdentityService; +use OpenCloud\Common\Auth\Token; use OpenCloud\Common\Service\Builder; -use OpenCloud\Identity\v2\Models\Token; -use OpenCloud\Identity\v2\Service as IdentityV2; -use OpenCloud\Identity\v3\Service as IdentityV3; -use OpenCloud\Compute\v2\Service as ComputeV2; -use OpenCloud\Test\Common\Auth\FakeToken; +use OpenCloud\Test\Common\Service\Fixtures; use OpenCloud\Test\TestCase; use Prophecy\Argument; @@ -22,11 +19,11 @@ public function setUp() $this->builder = new Builder([]); $this->opts = [ - 'username' => '1', - 'password' => '2', - 'tenantId' => '3', - 'authUrl' => '4', - 'region' => '5', + 'username' => '1', + 'password' => '2', + 'tenantId' => '3', + 'authUrl' => '4', + 'region' => '5', 'catalogName' => '6', 'catalogType' => '7', ]; @@ -37,7 +34,7 @@ public function setUp() */ public function test_it_throws_exception_if_username_is_missing() { - $this->builder->createService('Compute', 2, []); + $this->builder->createService('Compute\\v2', []); } /** @@ -45,7 +42,7 @@ public function test_it_throws_exception_if_username_is_missing() */ public function test_it_throws_exception_if_password_is_missing() { - $this->builder->createService('Compute', 2, ['username' => 1]); + $this->builder->createService('Compute\\v2', ['username' => 1]); } /** @@ -53,7 +50,7 @@ public function test_it_throws_exception_if_password_is_missing() */ public function test_it_throws_exception_if_both_tenantId_and_tenantName_is_missing() { - $this->builder->createService('Compute', 2, [ + $this->builder->createService('Compute\\v2', [ 'username' => 1, 'password' => 2, 'authUrl' => 4, 'region' => 5, 'catalogName' => 6, 'catalogType' => 7, ]); } @@ -63,7 +60,7 @@ public function test_it_throws_exception_if_both_tenantId_and_tenantName_is_miss */ public function test_it_throws_exception_if_authUrl_is_missing() { - $this->builder->createService('Compute', 2, ['username' => 1, 'password' => 2, 'tenantId' => 3]); + $this->builder->createService('Compute\\v2', ['username' => 1, 'password' => 2, 'tenantId' => 3]); } /** @@ -71,7 +68,7 @@ public function test_it_throws_exception_if_authUrl_is_missing() */ public function test_it_throws_exception_if_region_is_missing() { - $this->builder->createService('Compute', 2, [ + $this->builder->createService('Compute\\v2', [ 'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4, ]); } @@ -81,7 +78,7 @@ public function test_it_throws_exception_if_region_is_missing() */ public function test_it_throws_exception_if_catalogName_is_missing() { - $this->builder->createService('Compute', 2, [ + $this->builder->createService('Compute\\v2', [ 'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4, ]); } @@ -91,74 +88,53 @@ public function test_it_throws_exception_if_catalogName_is_missing() */ public function test_it_throws_exception_if_catalogType_is_missing() { - $this->builder->createService('Compute', 2, [ + $this->builder->createService('Compute\\v2', [ 'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4, 'region' => 5, 'catalogName' => 6, ]); } -// public function test_it_builds_services_with_custom_identity_service() -// { -// $this->rootFixturesDir = dirname(dirname(__DIR__)) . '/Identity/v2/'; -// -// $token = $this->prophesize(FakeToken::class)->reveal(); -// $service = $this->prophesize(IdentityService::class); -// $service->authenticate(Argument::type('array'))->shouldBeCalled()->willReturn([$token, '']); -// -// $this->opts += [ -// 'identityService' => $service->reveal(), -// 'catalogName' => 'nova', -// 'catalogType' => 'compute', -// 'region' => 'RegionOne', -// ]; -// -// $service = $this->builder->createService('Compute', 2, $this->opts); -// $this->assertInstanceOf(ComputeV2::class, $service); -// } - - private function setupHttpClient() + public function test_it_creates_service() { - $this->rootFixturesDir = dirname(dirname(__DIR__)) . '/Identity/v3/'; + $is = $this->prophesize(TestIdentity::class); + $is->authenticate(Argument::any())->willReturn([new FakeToken(), '']); - $response = $this->getFixture('token-get'); + $s = $this->builder->createService('Test\\Common\\Service\\Fixtures', $this->opts + [ + 'identityService' => $is->reveal(), + ]); - $expectedJson = [ - 'auth' => [ - 'identity' => [ - 'methods' => ['password'], - 'password' => ['user' => ['id' => '0ca8f6', 'password' => 'secretsecret']] - ] - ] - ]; + $this->assertInstanceOf(Fixtures\Service::class, $s); + } - $httpClient = $this->prophesize(ClientInterface::class); - $httpClient->request('POST', 'tokens', ['json' => $expectedJson])->shouldBeCalled()->willReturn($response); + public function test_it_does_not_authenticate_for_identity_services() + { + $is = $this->prophesize(TestIdentity::class); + $is->authenticate(Argument::any())->willReturn([new FakeToken(), '']); - return $httpClient; + $s = $this->builder->createService('Test\\Common\\Service\\Fixtures\\Identity', $this->opts + [ + 'identityService' => $is->reveal(), + ]); + + $this->assertInstanceOf(Fixtures\Identity\Service::class, $s); } +} - public function it_builds_services_with_default_identity() +class FakeToken implements Token +{ + public function getId(): string { - $httpClient = $this->setupHttpClient(); - - $options = [ - 'httpClient' => $httpClient->reveal(), - 'catalogName' => 'nova', - 'catalogType' => 'compute', - 'region' => 'RegionOne', - 'user' => [ - 'id' => '0ca8f6', - 'password' => 'secretsecret', - ] - ]; + return ''; + } - $service = $this->builder->createService('Compute', 2, $options); - $this->assertInstanceOf(ComputeV2::class, $service); + public function hasExpired(): bool + { + return false; } +} -// public function test_it_does_not_authenticate_when_creating_identity_services() -// { -// $this->assertInstanceOf(IdentityV3::class, $this->builder->createService('Identity', 3, [ -// 'authUrl' => 'foo.com', -// ])); -// } +class TestIdentity implements IdentityService +{ + public function authenticate(array $options): array + { + return []; + } } \ No newline at end of file diff --git a/tests/unit/Common/Service/Fixtures/Api.php b/tests/unit/Common/Service/Fixtures/Api.php new file mode 100644 index 0000000..7a74604 --- /dev/null +++ b/tests/unit/Common/Service/Fixtures/Api.php @@ -0,0 +1,9 @@ +getService(); + } +} \ No newline at end of file diff --git a/tests/unit/Common/Service/Fixtures/Service.php b/tests/unit/Common/Service/Fixtures/Service.php new file mode 100644 index 0000000..69a269f --- /dev/null +++ b/tests/unit/Common/Service/Fixtures/Service.php @@ -0,0 +1,9 @@ + ['foo' => true]]; - $json = $this->serializer->stockJson($param->reveal(), (object) ['foo' => true], []); + $json = $this->serializer->stockJson($param->reveal(), (object)['foo' => true], []); $this->assertEquals($expected, $json); } + + public function test_it_serializes_non_stdClass_objects() + { + $prop1 = $this->prophesize(Parameter::class); + $prop1->isArray()->shouldBeCalled()->willReturn(false); + $prop1->isObject()->shouldBeCalled()->willReturn(false); + $prop1->getName()->shouldBeCalled()->willReturn('id'); + $prop1->getPath()->shouldBeCalled()->willReturn(''); + + $prop2 = $this->prophesize(Parameter::class); + $prop2->isArray()->shouldBeCalled()->willReturn(false); + $prop2->isObject()->shouldBeCalled()->willReturn(false); + $prop2->getName()->shouldBeCalled()->willReturn('foo_name'); + $prop2->getPath()->shouldBeCalled()->willReturn(''); + + $prop3 = $this->prophesize(Parameter::class); + $prop3->isArray()->shouldBeCalled()->willReturn(false); + $prop3->isObject()->shouldBeCalled()->willReturn(false); + $prop3->getName()->shouldBeCalled()->willReturn('created_date'); + $prop3->getPath()->shouldBeCalled()->willReturn(''); + + $subParam = $this->prophesize(Parameter::class); + $subParam->isArray()->shouldBeCalled()->willReturn(false); + $subParam->isObject()->shouldBeCalled()->willReturn(true); + $subParam->getProperty('id')->shouldBeCalled()->willReturn($prop1); + $subParam->getProperty('fooName')->shouldBeCalled()->willReturn($prop2); + $subParam->getProperty('createdDate')->shouldBeCalled()->willReturn($prop3); + $subParam->getName()->shouldBeCalled()->willReturn('sub_resource'); + $subParam->getPath()->shouldBeCalled()->willReturn(''); + + $param = $this->prophesize(Parameter::class); + $param->isArray()->shouldBeCalled()->willReturn(false); + $param->isObject()->shouldBeCalled()->willReturn(true); + $param->getProperty('subResource')->shouldBeCalled()->willReturn($subParam); + $param->getName()->shouldBeCalled()->willReturn('resource'); + $param->getPath()->shouldBeCalled()->willReturn(''); + + $subResource = new SubResource(); + $subResource->id = 1; + $subResource->fooName = 2; + $subResource->createdDate = 3; + + $userValues = ['subResource' => $subResource]; + + $json = $this->serializer->stockJson($param->reveal(), $userValues, []); + + $expected = [ + 'resource' => [ + 'sub_resource' => [ + 'id' => 1, + 'foo_name' => 2, + 'created_date' => 3, + ], + ], + ]; + + $this->assertEquals($expected, $json); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function test_exception_is_thrown_when_non_stdClass_or_serializable_object_provided() + { + $subParam = $this->prophesize(Parameter::class); + $subParam->isArray()->shouldBeCalled()->willReturn(false); + $subParam->isObject()->shouldBeCalled()->willReturn(true); + $subParam->getName()->shouldBeCalled()->willReturn('sub_resource'); + $subParam->getPath()->shouldBeCalled()->willReturn(''); + + $param = $this->prophesize(Parameter::class); + $param->isArray()->shouldBeCalled()->willReturn(false); + $param->isObject()->shouldBeCalled()->willReturn(true); + $param->getProperty('subResource')->shouldBeCalled()->willReturn($subParam); + $param->getName()->shouldBeCalled()->willReturn('resource'); + $param->getPath()->shouldBeCalled()->willReturn(''); + + $userValues = ['subResource' => new NonSerializableResource()]; + + $this->serializer->stockJson($param->reveal(), $userValues, []); + } } + +class TestResource extends AbstractResource +{ + /** @var SubResource */ + public $subResource; +} + +class SubResource extends AbstractResource +{ + /** @var int */ + public $id; + + /** @var int */ + public $fooName; + + /** @var int */ + public $createdDate; +} + +class NonSerializableResource +{} \ No newline at end of file diff --git a/tests/unit/Common/Transport/RequestSerializerTest.php b/tests/unit/Common/Transport/RequestSerializerTest.php index cf06220..901b061 100644 --- a/tests/unit/Common/Transport/RequestSerializerTest.php +++ b/tests/unit/Common/Transport/RequestSerializerTest.php @@ -96,6 +96,23 @@ public function test_it_serializes_json() $this->assertEquals($expected, $actual); } + public function test_it_serializes_unescaped_json() + { + $sch = $this->prophesize(Parameter::class); + $sch->getLocation()->shouldBeCalled()->willReturn('json'); + + $op = $this->prophesize(Operation::class); + $op->getParam('foo')->shouldBeCalled()->willReturn($sch); + $op->getJsonKey()->shouldBeCalled()->willReturn(''); + + $this->js->stockJson($sch, 'bar/baz', [])->shouldBeCalled()->willReturn(['foo' => 'bar/baz']); + + $actual = $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar/baz']); + $expected = ['body' => '{"foo":"bar/baz"}', 'headers' => ['Content-Type' => 'application/json']]; + + $this->assertEquals($expected, $actual); + } + public function test_it_serializes_raw_vals() { $sch = $this->prophesize(Parameter::class); @@ -109,4 +126,18 @@ public function test_it_serializes_raw_vals() $this->assertEquals($expected, $actual); } + + public function test_it_does_serialize_unknown_locations() + { + $sch = $this->prophesize(Parameter::class); + $sch->getLocation()->shouldBeCalled()->willReturn('foo'); + + $op = $this->prophesize(Operation::class); + $op->getParam('foo')->shouldBeCalled()->willReturn($sch); + + $actual = $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar']); + $expected = ['headers' => []]; + + $this->assertEquals($expected, $actual); + } } From 37ed3c90d5e13f21a98227047888be7ae205c355 Mon Sep 17 00:00:00 2001 From: Jamie Hannaford Date: Fri, 1 Apr 2016 18:00:00 +0200 Subject: [PATCH 2/4] Add better integration tests --- src/Common/Api/OperatorInterface.php | 14 +- .../Api/{Operator.php => OperatorTrait.php} | 113 ++++++------- src/Common/Api/Parameter.php | 2 +- src/Common/HydratorStrategyTrait.php | 6 +- src/Common/Resource/AbstractResource.php | 112 ++++--------- src/Common/Resource/OperatorResource.php | 148 ++++++++++++++++++ src/Common/Resource/ResourceInterface.php | 9 ++ src/Common/Service/AbstractService.php | 6 +- src/Common/Service/Builder.php | 37 ++--- src/Common/Transport/JsonSerializer.php | 18 ++- src/Common/Transport/RequestSerializer.php | 14 +- src/Common/Transport/Serializable.php | 11 ++ 12 files changed, 307 insertions(+), 183 deletions(-) rename src/Common/Api/{Operator.php => OperatorTrait.php} (82%) create mode 100644 src/Common/Resource/OperatorResource.php create mode 100644 src/Common/Transport/Serializable.php diff --git a/src/Common/Api/OperatorInterface.php b/src/Common/Api/OperatorInterface.php index 168518b..8dfe7c3 100644 --- a/src/Common/Api/OperatorInterface.php +++ b/src/Common/Api/OperatorInterface.php @@ -44,11 +44,21 @@ public function execute(array $definition, array $userValues = []): ResponseInte public function executeAsync(array $definition, array $userValues = []): PromiseInterface; /** - * @param string $name The name of the model class. + * Retrieves a populated Operation according to the definition and values provided. A + * HTTP client is also injected into the object to allow it to communicate with the remote API. + * + * @param array $definition The data that dictates how the operation works + * + * @return Operation + */ + public function getOperation(array $definition): Operation; + + /** + * @param string $class The name of the model class. * @param mixed $data Either a {@see ResponseInterface} or data array that will populate the newly * created model class. * * @return \OpenCloud\Common\Resource\ResourceInterface */ - public function model(string $name, $data = null): ResourceInterface; + public function model(string $class, $data = null): ResourceInterface; } diff --git a/src/Common/Api/Operator.php b/src/Common/Api/OperatorTrait.php similarity index 82% rename from src/Common/Api/Operator.php rename to src/Common/Api/OperatorTrait.php index 5698779..4c37310 100644 --- a/src/Common/Api/Operator.php +++ b/src/Common/Api/OperatorTrait.php @@ -11,10 +11,7 @@ use OpenCloud\Common\Transport\RequestSerializer; use Psr\Http\Message\ResponseInterface; -/** - * {@inheritDoc} - */ -abstract class Operator implements OperatorInterface +trait OperatorTrait { /** @var ClientInterface */ protected $client; @@ -55,18 +52,58 @@ public function __debugInfo() } /** - * Retrieves a populated Operation according to the definition and values provided. A - * HTTP client is also injected into the object to allow it to communicate with the remote API. + * Magic method which intercepts async calls, finds the sequential version, and wraps it in a + * {@see Promise} object. In order for this to happen, the called methods need to be in the + * following format: `createAsync`, where `create` is the sequential method being wrapped. + * + * @param $methodName The name of the method being invoked. + * @param $args The arguments to be passed to the sequential method. * - * @param array $definition The data that dictates how the operation works + * @throws \RuntimeException If method does not exist * - * @return Operation + * @return Promise + */ + public function __call($methodName, $args) + { + $e = function ($name) { + return new \RuntimeException(sprintf('%s::%s is not defined', get_class($this), $name)); + }; + + if (substr($methodName, -5) === 'Async') { + $realMethod = substr($methodName, 0, -5); + if (!method_exists($this, $realMethod)) { + throw $e($realMethod); + } + + $promise = new Promise( + function () use (&$promise, $realMethod, $args) { + $value = call_user_func_array([$this, $realMethod], $args); + $promise->resolve($value); + } + ); + + return $promise; + } + + throw $e($methodName); + } + + /** + * {@inheritdoc} */ public function getOperation(array $definition): Operation { return new Operation($definition); } + /** + * @param Operation $operation + * @param array $userValues + * @param bool $async + * + * @return mixed + * @throws \Exception + */ protected function sendRequest(Operation $operation, array $userValues = [], bool $async = false) { $operation->validate($userValues); @@ -100,76 +137,16 @@ public function executeAsync(array $definition, array $userValues = []): Promise public function model(string $class, $data = null): ResourceInterface { $model = new $class($this->client, $this->api); - // @codeCoverageIgnoreStart if (!$model instanceof ResourceInterface) { throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class)); } // @codeCoverageIgnoreEnd - if ($data instanceof ResponseInterface) { $model->populateFromResponse($data); } elseif (is_array($data)) { $model->populateFromArray($data); } - return $model; } - - /** - * Will create a new instance of this class with the current HTTP client and API injected in. This - * is useful when enumerating over a collection since multiple copies of the same resource class - * are needed. - * - * @return static - */ - public function newInstance(): self - { - return new static($this->client, $this->api); - } - - /** - * @return \GuzzleHttp\Psr7\Uri:null - */ - protected function getHttpBaseUrl() - { - return $this->client->getConfig('base_uri'); - } - - /** - * Magic method which intercepts async calls, finds the sequential version, and wraps it in a - * {@see Promise} object. In order for this to happen, the called methods need to be in the - * following format: `createAsync`, where `create` is the sequential method being wrapped. - * - * @param $methodName The name of the method being invoked. - * @param $args The arguments to be passed to the sequential method. - * - * @throws \RuntimeException If method does not exist - * - * @return Promise - */ - public function __call($methodName, $args) - { - $e = function ($name) { - return new \RuntimeException(sprintf('%s::%s is not defined', get_class($this), $name)); - }; - - if (substr($methodName, -5) === 'Async') { - $realMethod = substr($methodName, 0, -5); - if (!method_exists($this, $realMethod)) { - throw $e($realMethod); - } - - $promise = new Promise( - function () use (&$promise, $realMethod, $args) { - $value = call_user_func_array([$this, $realMethod], $args); - $promise->resolve($value); - } - ); - - return $promise; - } - - throw $e($methodName); - } } diff --git a/src/Common/Api/Parameter.php b/src/Common/Api/Parameter.php index f1ec7c7..35dde11 100644 --- a/src/Common/Api/Parameter.php +++ b/src/Common/Api/Parameter.php @@ -240,7 +240,7 @@ private function validateObject($userValues) /** * Internal method which retrieves a nested property for object parameters. * - * @param $key The name of the child parameter + * @param string $key The name of the child parameter * * @returns Parameter * @throws \Exception diff --git a/src/Common/HydratorStrategyTrait.php b/src/Common/HydratorStrategyTrait.php index 5b87ad6..e25004e 100644 --- a/src/Common/HydratorStrategyTrait.php +++ b/src/Common/HydratorStrategyTrait.php @@ -15,17 +15,17 @@ trait HydratorStrategyTrait * @param array $data The data to set * @param array $aliases Any aliases */ - private function hydrate(array $data, array $aliases = []) + public function hydrate(array $data, array $aliases = []) { foreach ($data as $key => $val) { $key = isset($aliases[$key]) ? $aliases[$key] : $key; if (property_exists($this, $key)) { - $this->$key = $val; + $this->{$key} = $val; } } } - private function set(string $key, $property, array $data, callable $fn = null) + public function set(string $key, $property, array $data, callable $fn = null) { if (isset($data[$key]) && property_exists($this, $property)) { $value = $fn ? call_user_func($fn, $data[$key]) : $data[$key]; diff --git a/src/Common/Resource/AbstractResource.php b/src/Common/Resource/AbstractResource.php index 989fde7..fa0f836 100644 --- a/src/Common/Resource/AbstractResource.php +++ b/src/Common/Resource/AbstractResource.php @@ -2,7 +2,7 @@ namespace OpenCloud\Common\Resource; -use OpenCloud\Common\Api\Operator; +use OpenCloud\Common\Transport\Serializable; use OpenCloud\Common\Transport\Utils; use Psr\Http\Message\ResponseInterface; @@ -13,10 +13,8 @@ * * @package OpenCloud\Common\Resource */ -abstract class AbstractResource extends Operator implements ResourceInterface +abstract class AbstractResource implements ResourceInterface, Serializable { - const DEFAULT_MARKER_KEY = 'id'; - /** * The JSON key that indicates how the API nests singular resources. For example, when * performing a GET, it could respond with ``{"server": {"id": "12345"}}``. In this case, @@ -26,22 +24,6 @@ abstract class AbstractResource extends Operator implements ResourceInterface */ protected $resourceKey; - /** - * The key that indicates how the API nests resource collections. For example, when - * performing a GET, it could respond with ``{"servers": [{}, {}]}``. In this case, "servers" - * is the resources key, since the array of servers is nested inside. - * - * @var string - */ - protected $resourcesKey; - - /** - * Indicates which attribute of the current resource should be used for pagination markers. - * - * @var string - */ - protected $markerKey; - /** * An array of aliases that will be checked when the resource is being populated. For example, * @@ -58,7 +40,7 @@ abstract class AbstractResource extends Operator implements ResourceInterface * * @param ResponseInterface $response * - * @return $this|ResourceInterface + * @return AbstractResource */ public function populateFromResponse(ResponseInterface $response): self { @@ -166,80 +148,46 @@ protected function getAttrs(array $keys) return $output; } - /** - * @param array $definition - * - * @return mixed - */ - public function executeWithState(array $definition) + public function model(string $class, $data = null): ResourceInterface { - return $this->execute($definition, $this->getAttrs(array_keys($definition['params']))); - } + $model = new $class(); - private function getResourcesKey(): string - { - $resourcesKey = $this->resourcesKey; + // @codeCoverageIgnoreStart + if (!$model instanceof ResourceInterface) { + throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class)); + } + // @codeCoverageIgnoreEnd - if (!$resourcesKey) { - $class = substr(static::class, strrpos(static::class, '\\') + 1); - $resourcesKey = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $class)) . 's'; + if ($data instanceof ResponseInterface) { + $model->populateFromResponse($data); + } elseif (is_array($data)) { + $model->populateFromArray($data); } - return $resourcesKey; + return $model; } - /** - * {@inheritDoc} - */ - public function enumerate(array $def, array $userVals = [], callable $mapFn = null): \Generator + public function serialize(): \stdClass { - $operation = $this->getOperation($def); + $output = new \stdClass(); - $requestFn = function ($marker) use ($operation, $userVals) { - if ($marker) { - $userVals['marker'] = $marker; - } - return $this->sendRequest($operation, $userVals); - }; - - $resourceFn = function (array $data) { - $resource = $this->newInstance(); - $resource->populateFromArray($data); - return $resource; - }; - - $opts = [ - 'limit' => isset($userVals['limit']) ? $userVals['limit'] : null, - 'resourcesKey' => $this->getResourcesKey(), - 'markerKey' => $this->markerKey, - 'mapFn' => $mapFn, - ]; - - $iterator = new Iterator($opts, $requestFn, $resourceFn); - return $iterator(); - } + foreach ((new \ReflectionClass($this))->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + $val = $this->{$name}; - public function extractMultipleInstances(ResponseInterface $response, string $key = null): array - { - $key = $key ?: $this->getResourcesKey(); - $resourcesData = Utils::jsonDecode($response)[$key]; + $fn = function ($val) { + return ($val instanceof Serializable) ? $val->serialize() : $val; + }; - $resources = []; + if (is_array($val)) { + foreach ($val as $sk => $sv) { + $val[$sk] = $fn($sv); + } + } - foreach ($resourcesData as $resourceData) { - $resource = $this->newInstance(); - $resource->populateFromArray($resourceData); - $resources[] = $resource; + $output->{$name} = $fn($val); } - return $resources; - } - - protected function getService() - { - $class = static::class; - $service = substr($class, 0, strpos($class, 'Models') - 1) . '\\Service'; - - return new $service($this->client, $this->api); + return $output; } } diff --git a/src/Common/Resource/OperatorResource.php b/src/Common/Resource/OperatorResource.php new file mode 100644 index 0000000..6732c24 --- /dev/null +++ b/src/Common/Resource/OperatorResource.php @@ -0,0 +1,148 @@ +client, $this->api); + } + + /** + * @return \GuzzleHttp\Psr7\Uri:null + */ + protected function getHttpBaseUrl() + { + return $this->client->getConfig('base_uri'); + } + + /** + * @param array $definition + * + * @return mixed + */ + public function executeWithState(array $definition) + { + return $this->execute($definition, $this->getAttrs(array_keys($definition['params']))); + } + + private function getResourcesKey(): string + { + $resourcesKey = $this->resourcesKey; + + if (!$resourcesKey) { + $class = substr(static::class, strrpos(static::class, '\\') + 1); + $resourcesKey = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $class)) . 's'; + } + + return $resourcesKey; + } + + /** + * {@inheritDoc} + */ + public function enumerate(array $def, array $userVals = [], callable $mapFn = null): \Generator + { + $operation = $this->getOperation($def); + + $requestFn = function ($marker) use ($operation, $userVals) { + if ($marker) { + $userVals['marker'] = $marker; + } + return $this->sendRequest($operation, $userVals); + }; + + $resourceFn = function (array $data) { + $resource = $this->newInstance(); + $resource->populateFromArray($data); + return $resource; + }; + + $opts = [ + 'limit' => isset($userVals['limit']) ? $userVals['limit'] : null, + 'resourcesKey' => $this->getResourcesKey(), + 'markerKey' => $this->markerKey, + 'mapFn' => $mapFn, + ]; + + $iterator = new Iterator($opts, $requestFn, $resourceFn); + return $iterator(); + } + + public function extractMultipleInstances(ResponseInterface $response, string $key = null): array + { + $key = $key ?: $this->getResourcesKey(); + $resourcesData = Utils::jsonDecode($response)[$key]; + + $resources = []; + + foreach ($resourcesData as $resourceData) { + $resources[] = $this->newInstance()->populateFromArray($resourceData); + } + + return $resources; + } + + protected function getService() + { + $class = static::class; + $service = substr($class, 0, strpos($class, 'Models') - 1) . '\\Service'; + + return new $service($this->client, $this->api); + } + + /** + * {@inheritDoc} + */ + public function model(string $class, $data = null): ResourceInterface + { + $model = new $class($this->client, $this->api); + + // @codeCoverageIgnoreStart + if (!$model instanceof ResourceInterface) { + throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class)); + } + // @codeCoverageIgnoreEnd + + if ($data instanceof ResponseInterface) { + $model->populateFromResponse($data); + } elseif (is_array($data)) { + $model->populateFromArray($data); + } + + return $model; + } +} \ No newline at end of file diff --git a/src/Common/Resource/ResourceInterface.php b/src/Common/Resource/ResourceInterface.php index ffe3d20..43d52a1 100644 --- a/src/Common/Resource/ResourceInterface.php +++ b/src/Common/Resource/ResourceInterface.php @@ -26,4 +26,13 @@ public function populateFromResponse(ResponseInterface $response); * @return mixed */ public function populateFromArray(array $data); + + /** + * @param string $name The name of the model class. + * @param mixed $data Either a {@see ResponseInterface} or data array that will populate the newly + * created model class. + * + * @return \OpenCloud\Common\Resource\ResourceInterface + */ + public function model(string $class, $data = null): ResourceInterface; } diff --git a/src/Common/Service/AbstractService.php b/src/Common/Service/AbstractService.php index 660d7bb..b115c21 100644 --- a/src/Common/Service/AbstractService.php +++ b/src/Common/Service/AbstractService.php @@ -2,13 +2,15 @@ namespace OpenCloud\Common\Service; -use OpenCloud\Common\Api\Operator; +use OpenCloud\Common\Api\OperatorInterface; +use OpenCloud\Common\Api\OperatorTrait; /** * Represents the top-level abstraction of a service. * * @package OpenCloud\Common\Service */ -abstract class AbstractService extends Operator implements ServiceInterface +abstract class AbstractService implements ServiceInterface { + use OperatorTrait; } diff --git a/src/Common/Service/Builder.php b/src/Common/Service/Builder.php index 9ccd90d..823759d 100644 --- a/src/Common/Service/Builder.php +++ b/src/Common/Service/Builder.php @@ -46,22 +46,18 @@ public function __construct(array $globalOptions = [], $rootNamespace = 'OpenClo $this->rootNamespace = $rootNamespace; } - /** - * Internal method which resolves the API and Service classes for a service. - * - * @param string $serviceName The name of the service, e.g. Compute - * @param int $serviceVersion The major version of the service, e.g. 2 - * - * @return array - */ - private function getClasses(string $serviceName, int $serviceVersion) + private function getClasses($namespace) { - $rootNamespace = sprintf("%s\\%s\\v%d", $this->rootNamespace, $serviceName, $serviceVersion); + $namespace = $this->rootNamespace . '\\' . $namespace; + $classes = [$namespace.'\\Api', $namespace.'\\Service']; + + foreach ($classes as $class) { + if (!class_exists($class)) { + throw new \RuntimeException(sprintf("%s does not exist", $class)); + } + } - return [ - sprintf("%s\\Api", $rootNamespace), - sprintf("%s\\Service", $rootNamespace), - ]; + return $classes; } /** @@ -70,22 +66,21 @@ private function getClasses(string $serviceName, int $serviceVersion) * directly - this setup includes the configuration of the HTTP client's base URL, and the * attachment of an authentication handler. * - * @param string $serviceName The name of the service as it appears in the OpenCloud\* namespace - * @param int $serviceVersion The major version of the service + * @param string $namespace The namespace of the service * @param array $serviceOptions The service-specific options to use * * @return \OpenCloud\Common\Service\ServiceInterface * * @throws \Exception */ - public function createService(string $serviceName, int $serviceVersion, array $serviceOptions = []): ServiceInterface + public function createService(string $namespace, array $serviceOptions = []): ServiceInterface { $options = $this->mergeOptions($serviceOptions); $this->stockAuthHandler($options); - $this->stockHttpClient($options, $serviceName); + $this->stockHttpClient($options, $namespace); - list($apiClass, $serviceClass) = $this->getClasses($serviceName, $serviceVersion); + list($apiClass, $serviceClass) = $this->getClasses($namespace); return new $serviceClass($options['httpClient'], new $apiClass()); } @@ -93,7 +88,7 @@ public function createService(string $serviceName, int $serviceVersion, array $s private function stockHttpClient(array &$options, string $serviceName) { if (!isset($options['httpClient']) || !($options['httpClient'] instanceof ClientInterface)) { - if (strcasecmp($serviceName, 'identity') === 0) { + if (stripos($serviceName, 'identity') !== false) { $baseUrl = $options['authUrl']; $stack = $this->getStack($options['authHandler']); } else { @@ -129,7 +124,7 @@ private function stockAuthHandler(array &$options) { if (!isset($options['authHandler'])) { $options['authHandler'] = function () use ($options) { - return $options['identityService']->generateToken($options); + return $options['identityService']->authenticate($options)[0]; }; } } diff --git a/src/Common/Transport/JsonSerializer.php b/src/Common/Transport/JsonSerializer.php index 4d0b2e4..c4ef165 100644 --- a/src/Common/Transport/JsonSerializer.php +++ b/src/Common/Transport/JsonSerializer.php @@ -87,9 +87,25 @@ public function stockJson(Parameter $param, $userValue, array $json): array if ($param->isArray()) { $userValue = $this->stockArrayJson($param, $userValue); } elseif ($param->isObject()) { - $userValue = $this->stockObjectJson($param, (object) $userValue); + $userValue = $this->stockObjectJson($param, $this->serializeObjectValue($userValue)); } // Populate the final value return $this->stockValue($param, $userValue, $json); } + + private function serializeObjectValue($value) + { + if (is_object($value)) { + if ($value instanceof Serializable) { + $value = $value->serialize(); + } elseif (!($value instanceof \stdClass)) { + throw new \InvalidArgumentException(sprintf( + 'When an object value is provided, it must either be \stdClass or implement the Serializable ' + . 'interface, you provided %s', print_r($value, true) + )); + } + } + + return (object) $value; + } } diff --git a/src/Common/Transport/RequestSerializer.php b/src/Common/Transport/RequestSerializer.php index 61533fb..3319e62 100644 --- a/src/Common/Transport/RequestSerializer.php +++ b/src/Common/Transport/RequestSerializer.php @@ -26,8 +26,7 @@ public function serializeOptions(Operation $operation, array $userValues = []): continue; } - $method = sprintf('stock%s', ucfirst($schema->getLocation())); - $this->$method($schema, $paramValue, $options); + $this->callStockingMethod($schema, $paramValue, $options); } if (!empty($options['json'])) { @@ -44,8 +43,17 @@ public function serializeOptions(Operation $operation, array $userValues = []): return $options; } - private function stockUrl() + private function callStockingMethod(Parameter $schema, $paramValue, array &$options) { + $location = $schema->getLocation(); + + $methods = ['query', 'header', 'json', 'raw']; + if (!in_array($location, $methods)) { + return; + } + + $method = sprintf('stock%s', ucfirst($location)); + $this->$method($schema, $paramValue, $options); } private function stockQuery(Parameter $schema, $paramValue, array &$options) diff --git a/src/Common/Transport/Serializable.php b/src/Common/Transport/Serializable.php new file mode 100644 index 0000000..5ece12e --- /dev/null +++ b/src/Common/Transport/Serializable.php @@ -0,0 +1,11 @@ + Date: Mon, 11 Apr 2016 11:53:27 +0200 Subject: [PATCH 3/4] fix styling --- src/Common/Api/AbstractApi.php | 2 +- src/Common/Api/AbstractParams.php | 4 ++-- src/Common/Api/ApiInterface.php | 2 +- src/Common/Api/Operation.php | 4 ++-- src/Common/Api/OperatorInterface.php | 2 +- src/Common/Api/OperatorTrait.php | 2 +- src/Common/Api/Parameter.php | 2 +- src/Common/ArrayAccessTrait.php | 2 +- src/Common/Auth/AuthHandler.php | 2 +- src/Common/Auth/Catalog.php | 2 +- src/Common/Auth/IdentityService.php | 2 +- src/Common/Auth/Token.php | 2 +- src/Common/Error/BadResponseError.php | 2 +- src/Common/Error/BaseError.php | 2 +- src/Common/Error/Builder.php | 2 +- src/Common/Error/NotImplementedError.php | 2 +- src/Common/Error/UserInputError.php | 2 +- src/Common/HydratorStrategyTrait.php | 2 +- src/Common/JsonPath.php | 2 +- src/Common/JsonSchema/JsonPatch.php | 2 +- src/Common/JsonSchema/Schema.php | 2 +- src/Common/Resource/AbstractResource.php | 2 +- src/Common/Resource/Creatable.php | 2 +- src/Common/Resource/Deletable.php | 2 +- src/Common/Resource/HasMetadata.php | 2 +- src/Common/Resource/HasWaiterTrait.php | 2 +- src/Common/Resource/Iterator.php | 2 +- src/Common/Resource/Listable.php | 2 +- src/Common/Resource/OperatorResource.php | 2 +- src/Common/Resource/ResourceInterface.php | 2 +- src/Common/Resource/Retrievable.php | 2 +- src/Common/Resource/Updateable.php | 2 +- src/Common/Service/AbstractService.php | 2 +- src/Common/Service/Builder.php | 4 ++-- src/Common/Service/ServiceInterface.php | 2 +- src/Common/Transport/HandlerStack.php | 2 +- src/Common/Transport/JsonSerializer.php | 2 +- src/Common/Transport/Middleware.php | 2 +- src/Common/Transport/RequestSerializer.php | 2 +- src/Common/Transport/Serializable.php | 2 +- src/Common/Transport/Utils.php | 2 +- tests/integration/Runner.php | 3 +-- tests/unit/Common/Auth/AuthHandlerTest.php | 8 +++++--- tests/unit/Common/JsonSchema/SchemaTest.php | 2 +- tests/unit/Common/Resource/OperatorResourceTest.php | 3 ++- tests/unit/Common/Service/BuilderTest.php | 2 +- tests/unit/Common/Service/Fixtures/Api.php | 2 +- tests/unit/Common/Service/Fixtures/Identity/Api.php | 2 +- tests/unit/Common/Service/Fixtures/Identity/Service.php | 2 +- tests/unit/Common/Service/Fixtures/Models/Foo.php | 2 +- tests/unit/Common/Service/Fixtures/Service.php | 2 +- tests/unit/Common/Transport/JsonSerializerTest.php | 3 ++- 52 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/Common/Api/AbstractApi.php b/src/Common/Api/AbstractApi.php index 09988fd..d0cd5fb 100644 --- a/src/Common/Api/AbstractApi.php +++ b/src/Common/Api/AbstractApi.php @@ -1,4 +1,4 @@ - "Sorts by one or more sets of attribute and sort direction combinations.", ]; } -} \ No newline at end of file +} diff --git a/src/Common/Api/ApiInterface.php b/src/Common/Api/ApiInterface.php index d4629dc..015f270 100644 --- a/src/Common/Api/ApiInterface.php +++ b/src/Common/Api/ApiInterface.php @@ -1,4 +1,4 @@ -getOpts(); + list($serviceOpt, $versionOpt, $moduleOpt, $testMethodOpt, $verbosityOpt) = $this->getOpts(); foreach ($this->getRunnableServices($serviceOpt, $versionOpt, $moduleOpt) as $serviceName => $serviceArray) { foreach ($serviceArray as $versionName => $versionArray) { foreach ($versionArray as $testName) { - $this->logger->info(str_repeat('=', 49)); $this->logger->info("Starting %s %v %m integration test(s)", [ '%s' => $serviceName, diff --git a/tests/unit/Common/Auth/AuthHandlerTest.php b/tests/unit/Common/Auth/AuthHandlerTest.php index d0d63cf..964d28b 100644 --- a/tests/unit/Common/Auth/AuthHandlerTest.php +++ b/tests/unit/Common/Auth/AuthHandlerTest.php @@ -56,8 +56,10 @@ public function test_it_should_generate_a_new_token_if_the_current_token_is_eith class FakeToken implements Token { public function getId(): string - {} + { + } public function hasExpired(): bool - {} -} \ No newline at end of file + { + } +} diff --git a/tests/unit/Common/JsonSchema/SchemaTest.php b/tests/unit/Common/JsonSchema/SchemaTest.php index 4aae8e5..991d2a4 100644 --- a/tests/unit/Common/JsonSchema/SchemaTest.php +++ b/tests/unit/Common/JsonSchema/SchemaTest.php @@ -99,4 +99,4 @@ public function test_it_checks_validity() $this->schema->isValid(); } -} \ No newline at end of file +} diff --git a/tests/unit/Common/Resource/OperatorResourceTest.php b/tests/unit/Common/Resource/OperatorResourceTest.php index b487982..46f4d16 100644 --- a/tests/unit/Common/Resource/OperatorResourceTest.php +++ b/tests/unit/Common/Resource/OperatorResourceTest.php @@ -195,4 +195,5 @@ public function testBaseUri() } class Server extends OperatorResource -{} \ No newline at end of file +{ +} diff --git a/tests/unit/Common/Service/BuilderTest.php b/tests/unit/Common/Service/BuilderTest.php index 224ccdf..6c333a7 100644 --- a/tests/unit/Common/Service/BuilderTest.php +++ b/tests/unit/Common/Service/BuilderTest.php @@ -137,4 +137,4 @@ public function authenticate(array $options): array { return []; } -} \ No newline at end of file +} diff --git a/tests/unit/Common/Service/Fixtures/Api.php b/tests/unit/Common/Service/Fixtures/Api.php index 7a74604..4dac693 100644 --- a/tests/unit/Common/Service/Fixtures/Api.php +++ b/tests/unit/Common/Service/Fixtures/Api.php @@ -6,4 +6,4 @@ class Api extends AbstractApi { -} \ No newline at end of file +} diff --git a/tests/unit/Common/Service/Fixtures/Identity/Api.php b/tests/unit/Common/Service/Fixtures/Identity/Api.php index 3f97460..236aa02 100644 --- a/tests/unit/Common/Service/Fixtures/Identity/Api.php +++ b/tests/unit/Common/Service/Fixtures/Identity/Api.php @@ -6,4 +6,4 @@ class Api extends AbstractApi { -} \ No newline at end of file +} diff --git a/tests/unit/Common/Service/Fixtures/Identity/Service.php b/tests/unit/Common/Service/Fixtures/Identity/Service.php index 3c9f663..804ac5e 100644 --- a/tests/unit/Common/Service/Fixtures/Identity/Service.php +++ b/tests/unit/Common/Service/Fixtures/Identity/Service.php @@ -6,4 +6,4 @@ class Service extends AbstractService { -} \ No newline at end of file +} diff --git a/tests/unit/Common/Service/Fixtures/Models/Foo.php b/tests/unit/Common/Service/Fixtures/Models/Foo.php index ac3b5a0..9141313 100644 --- a/tests/unit/Common/Service/Fixtures/Models/Foo.php +++ b/tests/unit/Common/Service/Fixtures/Models/Foo.php @@ -10,4 +10,4 @@ public function testGetService() { return $this->getService(); } -} \ No newline at end of file +} diff --git a/tests/unit/Common/Service/Fixtures/Service.php b/tests/unit/Common/Service/Fixtures/Service.php index 69a269f..8aa4135 100644 --- a/tests/unit/Common/Service/Fixtures/Service.php +++ b/tests/unit/Common/Service/Fixtures/Service.php @@ -6,4 +6,4 @@ class Service extends AbstractService { -} \ No newline at end of file +} diff --git a/tests/unit/Common/Transport/JsonSerializerTest.php b/tests/unit/Common/Transport/JsonSerializerTest.php index aa11f4d..b14f6a8 100644 --- a/tests/unit/Common/Transport/JsonSerializerTest.php +++ b/tests/unit/Common/Transport/JsonSerializerTest.php @@ -188,4 +188,5 @@ class SubResource extends AbstractResource } class NonSerializableResource -{} \ No newline at end of file +{ +} From 1bf701bf9039cb6d7a185ebe4a3fad02d0b1f0be Mon Sep 17 00:00:00 2001 From: Jamie Hannaford Date: Mon, 11 Apr 2016 11:59:39 +0200 Subject: [PATCH 4/4] fix test --- tests/unit/Common/Transport/JsonSerializerTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit/Common/Transport/JsonSerializerTest.php b/tests/unit/Common/Transport/JsonSerializerTest.php index b14f6a8..ff0de07 100644 --- a/tests/unit/Common/Transport/JsonSerializerTest.php +++ b/tests/unit/Common/Transport/JsonSerializerTest.php @@ -153,15 +153,11 @@ public function test_exception_is_thrown_when_non_stdClass_or_serializable_objec $subParam = $this->prophesize(Parameter::class); $subParam->isArray()->shouldBeCalled()->willReturn(false); $subParam->isObject()->shouldBeCalled()->willReturn(true); - $subParam->getName()->shouldBeCalled()->willReturn('sub_resource'); - $subParam->getPath()->shouldBeCalled()->willReturn(''); $param = $this->prophesize(Parameter::class); $param->isArray()->shouldBeCalled()->willReturn(false); $param->isObject()->shouldBeCalled()->willReturn(true); $param->getProperty('subResource')->shouldBeCalled()->willReturn($subParam); - $param->getName()->shouldBeCalled()->willReturn('resource'); - $param->getPath()->shouldBeCalled()->willReturn(''); $userValues = ['subResource' => new NonSerializableResource()];