diff --git a/src/Core/Exception/NotFoundException.php b/src/Core/Exception/NotFoundException.php index f0a1f3139ed6..e6ba2c5cbe59 100644 --- a/src/Core/Exception/NotFoundException.php +++ b/src/Core/Exception/NotFoundException.php @@ -22,5 +22,15 @@ */ class NotFoundException extends ServiceException { - + /** + * Allows overriding message for injection of Whitelist Notice. + * + * @param string $message the new message + * @return void + * @access private + */ + public function setMessage($message) + { + $this->message = $message; + } } diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index 3d89d417a9e0..51cb3a3f1a07 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -216,6 +216,7 @@ private function convertToGoogleException(ApiException $ex) break; case Grpc\STATUS_NOT_FOUND: + case Grpc\STATUS_UNIMPLEMENTED: $exception = Exception\NotFoundException::class; break; diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index 46313151a373..01a655e3e5d9 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -19,9 +19,10 @@ use DateTime; use DateTimeZone; -use Google\Auth\FetchAuthTokenCache; use Google\Auth\Cache\MemoryCacheItemPool; +use Google\Auth\FetchAuthTokenCache; use Google\Cloud\Core\ArrayTrait; +use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\GrpcRequestWrapper; use google\protobuf; @@ -31,6 +32,7 @@ trait GrpcTrait { use ArrayTrait; + use WhitelistTrait; /** * @var GrpcRequestWrapper Wrapper used to handle sending requests to the @@ -53,9 +55,10 @@ public function setRequestWrapper(GrpcRequestWrapper $requestWrapper) * * @param callable $request * @param array $args + * @param bool $whitelisted * @return \Generator|array */ - public function send(callable $request, array $args) + public function send(callable $request, array $args, $whitelisted = false) { $requestOptions = $this->pluckArray([ 'grpcOptions', @@ -63,7 +66,15 @@ public function send(callable $request, array $args) 'requestTimeout' ], $args[count($args) - 1]); - return $this->requestWrapper->send($request, $args, $requestOptions); + try { + return $this->requestWrapper->send($request, $args, $requestOptions); + } catch (NotFoundException $e) { + if ($whitelisted) { + throw $this->modifyWhitelistedError($e); + } + + throw $e; + } } /** diff --git a/src/Core/RestTrait.php b/src/Core/RestTrait.php index 663687a807e9..1640e5baad55 100644 --- a/src/Core/RestTrait.php +++ b/src/Core/RestTrait.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Core; +use Google\Cloud\Core\Exception\NotFoundException; + /** * Provides shared functionality for REST service implementations. */ @@ -24,6 +26,7 @@ trait RestTrait { use ArrayTrait; use JsonTrait; + use WhitelistTrait; /** * @var RequestBuilder Builds PSR7 requests from a service definition. @@ -64,9 +67,10 @@ public function setRequestWrapper(RequestWrapper $requestWrapper) * @param string $resource The resource type used for the request. * @param string $method The method used for the request. * @param array $options [optional] Options used to build out the request. + * @param array $whitelisted [optional] * @return array */ - public function send($resource, $method, array $options = []) + public function send($resource, $method, array $options = [], $whitelisted = false) { $requestOptions = $this->pluckArray([ 'restOptions', @@ -74,12 +78,20 @@ public function send($resource, $method, array $options = []) 'requestTimeout' ], $options); - return json_decode( - $this->requestWrapper->send( - $this->requestBuilder->build($resource, $method, $options), - $requestOptions - )->getBody(), - true - ); + try { + return json_decode( + $this->requestWrapper->send( + $this->requestBuilder->build($resource, $method, $options), + $requestOptions + )->getBody(), + true + ); + } catch (NotFoundException $e) { + if ($whitelisted) { + throw $this->modifyWhitelistedError($e); + } + + throw $e; + } } } diff --git a/src/Core/WhitelistTrait.php b/src/Core/WhitelistTrait.php new file mode 100644 index 000000000000..5e40e6bef7a3 --- /dev/null +++ b/src/Core/WhitelistTrait.php @@ -0,0 +1,39 @@ +setMessage('NOTE: Error may be due to Whitelist Restriction. ' . $e->getMessage()); + + return $e; + } +} diff --git a/src/PubSub/Connection/Grpc.php b/src/PubSub/Connection/Grpc.php index 445c7a6be946..903de3fc860e 100644 --- a/src/PubSub/Connection/Grpc.php +++ b/src/PubSub/Connection/Grpc.php @@ -288,10 +288,11 @@ public function acknowledge(array $args) */ public function listSnapshots(array $args) { + $whitelisted = true; return $this->send([$this->subscriberClient, 'listSnapshots'], [ $this->pluck('project', $args), $args - ]); + ], $whitelisted); } /** @@ -299,11 +300,12 @@ public function listSnapshots(array $args) */ public function createSnapshot(array $args) { + $whitelisted = true; return $this->send([$this->subscriberClient, 'createSnapshot'], [ $this->pluck('name', $args), $this->pluck('subscription', $args), $args - ]); + ], $whitelisted); } /** @@ -311,10 +313,11 @@ public function createSnapshot(array $args) */ public function deleteSnapshot(array $args) { + $whitelisted = true; return $this->send([$this->subscriberClient, 'deleteSnapshot'], [ $this->pluck('snapshot', $args), $args - ]); + ], $whitelisted); } /** @@ -327,10 +330,11 @@ public function seek(array $args) $args['time'] = (new protobuf\Timestamp)->deserialize($time, $this->codec); } + $whitelisted = true; return $this->send([$this->subscriberClient, 'seek'], [ $this->pluck('subscription', $args), $args - ]); + ], $whitelisted); } /** diff --git a/src/PubSub/Connection/Rest.php b/src/PubSub/Connection/Rest.php index 44bbb65e7fcc..d45e9b4b2d15 100644 --- a/src/PubSub/Connection/Rest.php +++ b/src/PubSub/Connection/Rest.php @@ -214,7 +214,8 @@ public function acknowledge(array $args) */ public function listSnapshots(array $args) { - return $this->send('snapshots', 'list', $args); + $whitelisted = true; + return $this->send('snapshots', 'list', $args, $whitelisted); } /** @@ -222,7 +223,8 @@ public function listSnapshots(array $args) */ public function createSnapshot(array $args) { - return $this->send('snapshots', 'create', $args); + $whitelisted = true; + return $this->send('snapshots', 'create', $args, $whitelisted); } /** @@ -230,7 +232,8 @@ public function createSnapshot(array $args) */ public function deleteSnapshot(array $args) { - return $this->send('snapshots', 'delete', $args); + $whitelisted = true; + return $this->send('snapshots', 'delete', $args, $whitelisted); } /** @@ -238,7 +241,8 @@ public function deleteSnapshot(array $args) */ public function seek(array $args) { - return $this->send('subscriptions', 'seek', $args); + $whitelisted = true; + return $this->send('subscriptions', 'seek', $args, $whitelisted); } /** diff --git a/tests/system/ServiceWhitelist/WhitelistTest.php b/tests/system/ServiceWhitelist/WhitelistTest.php new file mode 100644 index 000000000000..cbb86b347cc7 --- /dev/null +++ b/tests/system/ServiceWhitelist/WhitelistTest.php @@ -0,0 +1,182 @@ +markTestSkipped('Missing whitelist keyfile path for whitelist system tests.'); + } + + $this->keyFilePath = GOOGLE_CLOUD_WHITELIST_KEY_PATH; + } + + public function testPubSubListSnapshotsRest() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'rest' + ]); + + $this->checkException(function() use ($client) { + iterator_to_array($client->snapshots()); + }); + } + + public function testPubSubListSnapshotsGrpc() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'grpc' + ]); + + $this->checkException(function() use ($client) { + iterator_to_array($client->snapshots()); + }); + } + + public function testPubSubCreateSnapshotRest() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'rest' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($client, $sub) { + $client->createSnapshot(uniqid(self::TESTING_PREFIX), $sub); + }); + } + + public function testPubSubCreateSnapshotGrpc() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'grpc' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($client, $sub) { + $client->createSnapshot(uniqid(self::TESTING_PREFIX), $sub); + }); + } + + public function testPubSubSeekRest() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'rest' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($sub) { + $sub->seekToTime(new Timestamp(new \DateTime)); + }); + } + + public function testPubSubSeekGrpc() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'grpc' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($sub) { + $sub->seekToTime(new Timestamp(new \DateTime)); + }); + } + + private function checkException(callable $call) + { + $thrown = false; + $ex = null; + try { + $call(); + } catch (\Exception $e) { + $thrown = true; + $ex = $e; + } + + $this->assertTrue($thrown); + $this->assertInstanceOf(NotFoundException::class, $ex); + $this->assertTrue(strpos($ex->getMessage(), self::MESSAGE) !== false); + } + + public static function tearDownFixtures() + { + foreach (self::$deletionQueue as $toDelete) { + if (!is_callable($toDelete)) { + throw new \Exception('fixtures must be callables'); + } + + call_user_func($toDelete); + } + } +} diff --git a/tests/system/bootstrap.php b/tests/system/bootstrap.php index f9e2b36a997c..528369062539 100644 --- a/tests/system/bootstrap.php +++ b/tests/system/bootstrap.php @@ -8,6 +8,7 @@ use Google\Cloud\Tests\System\PubSub\PubSubTestCase; use Google\Cloud\Tests\System\Spanner\SpannerTestCase; use Google\Cloud\Tests\System\Storage\StorageTestCase; +use Google\Cloud\Tests\System\Whitelist\WhitelistTest; if (!getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH')) { throw new \Exception( @@ -15,6 +16,10 @@ ); } +if (getenv('GOOGLE_CLOUD_PHP_TESTS_WHITELIST_KEY_PATH')) { + define('GOOGLE_CLOUD_WHITELIST_KEY_PATH', getenv('GOOGLE_CLOUD_PHP_TESTS_WHITELIST_KEY_PATH')); +} + register_shutdown_function(function () { PubSubTestCase::tearDownFixtures(); DatastoreTestCase::tearDownFixtures(); @@ -22,4 +27,5 @@ LoggingTestCase::tearDownFixtures(); BigQueryTestCase::tearDownFixtures(); SpannerTestCase::tearDownFixtures(); + WhitelistTest::tearDownFixtures(); }); diff --git a/tests/unit/Core/Exception/NotFoundExceptionTest.php b/tests/unit/Core/Exception/NotFoundExceptionTest.php new file mode 100644 index 000000000000..72dd944a5f67 --- /dev/null +++ b/tests/unit/Core/Exception/NotFoundExceptionTest.php @@ -0,0 +1,36 @@ +assertEquals($ex->getMessage(), 'hello'); + + $ex->setMessage('world'); + $this->assertEquals($ex->getMessage(), 'world'); + } +} diff --git a/tests/unit/Core/WhitelistTraitTest.php b/tests/unit/Core/WhitelistTraitTest.php new file mode 100644 index 000000000000..dd654c205919 --- /dev/null +++ b/tests/unit/Core/WhitelistTraitTest.php @@ -0,0 +1,58 @@ +trait = new WhitelistTraitStub; + } + + public function testModifyWhitelistedError() + { + $ex = new NotFoundException('hello world'); + + $res = $this->trait->call('modifyWhitelistedError', [$ex]); + + $this->assertInstanceOf(NotFoundException::class, $res); + $this->assertEquals( + $res->getMessage(), + 'NOTE: Error may be due to Whitelist Restriction. hello world' + ); + } +} + +class WhitelistTraitStub +{ + use WhitelistTrait; + + public function call($method, array $args) + { + return call_user_func_array([$this, $method], $args); + } +}