diff --git a/README.md b/README.md index 23b072e..4c15678 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,16 @@ Please check the [Laravel support policy](https://laravel.com/docs/master/releas 'queue' => env('STACKKIT_CLOUD_TASKS_QUEUE', 'default'), 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), + + // Required when using AppEngine + 'app_engine' => env('STACKKIT_APP_ENGINE_TASK', false), + 'app_engine_service' => env('STACKKIT_APP_ENGINE_SERVICE', ''), + + // Required when not using AppEngine + 'handler' => env('STACKKIT_CLOUD_TASKS_HANDLER', ''), + 'service_account_email' => env('STACKKIT_CLOUD_TASKS_SERVICE_EMAIL', ''), + 'signed_audience' => env('STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE', true), + // Optional: The deadline in seconds for requests sent to the worker. If the worker // does not respond by this deadline then the request is cancelled and the attempt // is marked as a DEADLINE_EXCEEDED failure. @@ -64,13 +74,17 @@ Now that the package is installed, the final step is to set the correct environm Please check the table below on what the values mean and what their value should be. -| Environment variable | Description |Example ---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--- -| `STACKKIT_CLOUD_TASKS_PROJECT` | The project your queue belongs to. |`my-project` -| `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted |`europe-west6` -| `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to |`emails` -| `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` -| `STACKKIT_CLOUD_TASKS_HANDLER` (optional) | The URL that Cloud Tasks will call to process a job. This should be the URL to your Laravel app. By default we will use the URL that dispatched the job. |`https://.com` +| Environment variable | Description |Example +---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|--- +| `STACKKIT_CLOUD_TASKS_PROJECT` | The project your queue belongs to. |`my-project` +| `STACKKIT_CLOUD_TASKS_LOCATION` | The region where the project is hosted. |`europe-west6` +| `STACKKIT_CLOUD_TASKS_QUEUE` | The default queue a job will be added to. |`emails` +| **App Engine** +| `STACKKIT_APP_ENGINE_TASK` (optional) | Set to true to use App Engine task (else a Http task will be used). Defaults to false. |`true` +| `STACKKIT_APP_ENGINE_SERVICE` (optional) | The App Engine service to handle the task (only if using App Engine task). |`api` +| **Non- App Engine apps** +| `STACKKIT_CLOUD_TASKS_SERVICE_EMAIL` (optional) | The email address of the service account. Important, it should have the correct roles. See the section below which roles. |`my-service-account@appspot.gserviceaccount.com` +| `STACKKIT_CLOUD_TASKS_HANDLER` (optional) | The URL that Cloud Tasks will call to process a job. This should be the URL to your Laravel app. By default we will use the URL that dispatched the job. |`https://.com` | `STACKKIT_CLOUD_TASKS_SIGNED_AUDIENCE` (optional) | True or false depending if you want extra security by signing the audience of your tasks. May misbehave in certain Cloud Run setups. Defaults to true. | `true`
diff --git a/src/CloudTasksConnector.php b/src/CloudTasksConnector.php index db81cd6..8ff23e9 100644 --- a/src/CloudTasksConnector.php +++ b/src/CloudTasksConnector.php @@ -9,8 +9,6 @@ class CloudTasksConnector implements ConnectorInterface { public function connect(array $config) { - Config::validate($config); - // The handler is the URL which Cloud Tasks will call with the job payload. This // URL of the handler can be manually set through an environment variable, but // if it is not then we will choose a sensible default (the current app url) diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index b07242b..ccb7798 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -2,6 +2,8 @@ namespace Stackkit\LaravelGoogleCloudTasksQueue; +use Google\Cloud\Tasks\V2\AppEngineHttpRequest; +use Google\Cloud\Tasks\V2\AppEngineRouting; use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\HttpMethod; use Google\Cloud\Tasks\V2\HttpRequest; @@ -14,6 +16,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; + use function Safe\json_decode; use function Safe\json_encode; @@ -36,7 +39,7 @@ public function __construct(array $config, CloudTasksClient $client, $dispatchAf /** * Get the size of the queue. * - * @param string|null $queue + * @param string|null $queue * @return int */ public function size($queue = null) @@ -48,11 +51,11 @@ public function size($queue = null) /** * Fallback method for Laravel 6x and 7x * - * @param \Closure|string|object $job - * @param string $payload - * @param string $queue - * @param \DateTimeInterface|\DateInterval|int|null $delay - * @param callable $callback + * @param \Closure|string|object $job + * @param string $payload + * @param string $queue + * @param \DateTimeInterface|\DateInterval|int|null $delay + * @param callable $callback * @return mixed */ protected function enqueueUsing($job, $payload, $queue, $delay, $callback) @@ -67,9 +70,9 @@ protected function enqueueUsing($job, $payload, $queue, $delay, $callback) /** * Push a new job onto the queue. * - * @param string|object $job - * @param mixed $data - * @param string|null $queue + * @param string|object $job + * @param mixed $data + * @param string|null $queue * @return void */ public function push($job, $data = '', $queue = null) @@ -88,14 +91,14 @@ function ($payload, $queue) { /** * Push a raw payload onto the queue. * - * @param string $payload - * @param string|null $queue - * @param array $options + * @param string $payload + * @param string|null $queue + * @param array $options * @return string */ public function pushRaw($payload, $queue = null, array $options = []) { - $delay = ! empty($options['delay']) ? $options['delay'] : 0; + $delay = !empty($options['delay']) ? $options['delay'] : 0; return $this->pushToCloudTasks($queue, $payload, $delay); } @@ -103,10 +106,10 @@ public function pushRaw($payload, $queue = null, array $options = []) /** * Push a new job onto the queue after a delay. * - * @param \DateTimeInterface|\DateInterval|int $delay - * @param string|object $job - * @param mixed $data - * @param string|null $queue + * @param \DateTimeInterface|\DateInterval|int $delay + * @param string|object $job + * @param mixed $data + * @param string|null $queue * @return void */ public function later($delay, $job, $data = '', $queue = null) @@ -125,8 +128,8 @@ function ($payload, $queue, $delay) { /** * Push a job to Cloud Tasks. * - * @param string|null $queue - * @param string $payload + * @param string|null $queue + * @param string $payload * @param \DateTimeInterface|\DateInterval|int $delay * @return string */ @@ -136,10 +139,6 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $queueName = $this->client->queueName($this->config['project'], $this->config['location'], $queue); $availableAt = $this->availableAt($delay); - $httpRequest = $this->createHttpRequest(); - $httpRequest->setUrl($this->getHandler()); - $httpRequest->setHttpMethod(HttpMethod::POST); - $payload = json_decode($payload, true); // Laravel 7+ jobs have a uuid, but Laravel 6 doesn't have it. @@ -152,11 +151,38 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) // value and need to manually set and update the number of times a task has been attempted. $payload = $this->withAttempts($payload); - $httpRequest->setBody(json_encode($payload)); - $task = $this->createTask(); $task->setName($this->taskName($queue, $payload)); - $task->setHttpRequest($httpRequest); + + if (!empty($this->config['app_engine'])) { + $path = \Safe\parse_url(route('cloud-tasks.handle-task'), PHP_URL_PATH); + + $appEngineRequest = new AppEngineHttpRequest(); + $appEngineRequest->setRelativeUri($path); + $appEngineRequest->setHttpMethod(HttpMethod::POST); + $appEngineRequest->setBody(json_encode($payload)); + if (!empty($service = $this->config['app_engine_service'])) { + $routing = new AppEngineRouting(); + $routing->setService($service); + $appEngineRequest->setAppEngineRouting($routing); + } + $task->setAppEngineHttpRequest($appEngineRequest); + } else { + $httpRequest = $this->createHttpRequest(); + $httpRequest->setUrl($this->getHandler()); + $httpRequest->setHttpMethod(HttpMethod::POST); + + $httpRequest->setBody(json_encode($payload)); + + $token = new OidcToken; + $token->setServiceAccountEmail($this->config['service_account_email']); + if ($audience = $this->getAudience()) { + $token->setAudience($audience); + } + $httpRequest->setOidcToken($token); + $task->setHttpRequest($httpRequest); + } + // The deadline for requests sent to the app. If the app does not respond by // this deadline then the request is cancelled and the attempt is marked as @@ -165,13 +191,6 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) $task->setDispatchDeadline(new Duration(['seconds' => $this->config['dispatch_deadline']])); } - $token = new OidcToken; - $token->setServiceAccountEmail($this->config['service_account_email']); - if ($audience = $this->getAudience()) { - $token->setAudience($audience); - } - $httpRequest->setOidcToken($token); - if ($availableAt > time()) { $task->setScheduleTime(new Timestamp(['seconds' => $availableAt])); } @@ -186,7 +205,7 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) private function withUuid(array $payload): array { if (!isset($payload['uuid'])) { - $payload['uuid'] = (string) Str::uuid(); + $payload['uuid'] = (string)Str::uuid(); } return $payload; @@ -227,7 +246,7 @@ private function withAttempts(array $payload): array /** * Pop the next job off of the queue. * - * @param string|null $queue + * @param string|null $queue * @return \Illuminate\Contracts\Queue\Job|null */ public function pop($queue = null) @@ -251,11 +270,13 @@ public function delete(CloudTasksJob $job): void $queue = $job->getQueue() ?: $this->config['queue']; // @todo: make this a helper method somewhere. + $headerTaskName = request()->headers->get('X-Cloudtasks-Taskname') + ?? request()->headers->get('X-AppEngine-TaskName'); $taskName = $this->client->taskName( $config['project'], $config['location'], $queue, - (string) request()->headers->get('X-Cloudtasks-Taskname') + (string)$headerTaskName ); CloudTasksApi::deleteTask($taskName); diff --git a/src/Config.php b/src/Config.php index 422ce44..9819d6e 100644 --- a/src/Config.php +++ b/src/Config.php @@ -9,21 +9,6 @@ class Config { - public static function validate(array $config): void - { - if (empty($config['project'])) { - throw new Error(Errors::invalidProject()); - } - - if (empty($config['location'])) { - throw new Error(Errors::invalidLocation()); - } - - if (empty($config['service_account_email'])) { - throw new Error(Errors::invalidServiceAccountEmail()); - } - } - /** * @param Closure|string $handler */ @@ -42,25 +27,29 @@ public static function getHandler($handler): string // (still) set to localhost. That will never work because Cloud Tasks // should always call a public address / hostname to process tasks. if (in_array($parse['host'], ['localhost', '127.0.0.1', '::1'])) { - throw new Exception(sprintf( - 'Unable to push task to Cloud Tasks because the handler URL is set to a local host: %s. ' . - 'This does not work because Google is not able to call the given local URL. ' . - 'If you are developing on locally, consider using Ngrok or Expose for Laravel to expose your local ' . - 'application to the internet.', - $handler - )); + throw new Exception( + sprintf( + 'Unable to push task to Cloud Tasks because the handler URL is set to a local host: %s. ' . + 'This does not work because Google is not able to call the given local URL. ' . + 'If you are developing on locally, consider using Ngrok or Expose for Laravel to expose your local ' . + 'application to the internet.', + $handler + ) + ); } // When the application is running behind a proxy and the TrustedProxy middleware has not been set up yet, // an error like [HttpRequest.url must start with 'https'] could be thrown. Since the handler URL must // always be https, we will provide a little extra information on how to fix this. if ($parse['scheme'] !== 'https') { - throw new Exception(sprintf( - 'Unable to push task to Cloud Tasks because the hander URL is not https. Google Cloud Tasks ' . - 'will only call safe (https) URLs. If you are running Laravel behind a proxy (e.g. Ngrok, Expose), make sure it is ' . - 'as a trusted proxy. To quickly fix this, add the following to the [app/Http/Middleware/TrustProxies] middleware: ' . - 'protected $proxies = \'*\';' - )); + throw new Exception( + sprintf( + 'Unable to push task to Cloud Tasks because the hander URL is not https. Google Cloud Tasks ' . + 'will only call safe (https) URLs. If you are running Laravel behind a proxy (e.g. Ngrok, Expose), make sure it is ' . + 'as a trusted proxy. To quickly fix this, add the following to the [app/Http/Middleware/TrustProxies] middleware: ' . + 'protected $proxies = \'*\';' + ) + ); } $trimmedHandlerUrl = rtrim($handler, '/'); diff --git a/src/DashboardService.php b/src/DashboardService.php index 8800e3d..4dc5cec 100644 --- a/src/DashboardService.php +++ b/src/DashboardService.php @@ -21,9 +21,9 @@ public static function make(): DashboardService private function getTaskBody(Task $task): string { - $httpRequest = $task->getHttpRequest(); + $httpRequest = $task->getHttpRequest() ?: $task->getAppEngineHttpRequest(); - if (! $httpRequest instanceof HttpRequest) { + if (! $httpRequest) { throw new Exception('Task does not have a HTTP request.'); } diff --git a/src/Errors.php b/src/Errors.php index 58c648c..1d73f64 100644 --- a/src/Errors.php +++ b/src/Errors.php @@ -18,4 +18,9 @@ public static function invalidServiceAccountEmail(): string { return 'Google Service Account email address not provided. This is needed to secure the handler so it is only accessible by Google. To fix this, set the STACKKIT_CLOUD_TASKS_SERVICE_EMAIL environment variable'; } + + public static function serviceAccountOrAppEngine(): string + { + return 'A Google Service Account email or App Engine Request must be set. Set STACKKIT_CLOUD_TASKS_SERVICE_EMAIL or STACKKIT_APP_ENGINE_TASK'; + } } diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 434e8f8..8d3a40e 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -5,15 +5,14 @@ use Google\ApiCore\ApiException; use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\RetryConfig; -use Illuminate\Bus\Queueable; use Illuminate\Contracts\Encryption\Encrypter; -use Illuminate\Queue\Jobs\Job; -use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Safe\Exceptions\JsonException; use UnexpectedValueException; +use stdClass; + use function Safe\json_decode; class TaskHandler @@ -51,7 +50,9 @@ public function handle(?string $task = null): void $this->setQueue(); - OpenIdVerificator::verify(request()->bearerToken(), $this->config); + if (empty($this->config['app_engine'])) { + OpenIdVerificator::verify(request()->bearerToken(), $this->config); + } $this->handleTask($task); } @@ -63,7 +64,7 @@ public function handle(?string $task = null): void */ private function captureTask($task): array { - $task = $task ?: (string) (request()->getContent()); + $task = $task ?: (string)(request()->getContent()); try { $array = json_decode($task, true); @@ -72,13 +73,13 @@ private function captureTask($task): array } $validator = validator([ - 'json' => $task, - 'task' => $array, - 'name_header' => request()->header('X-CloudTasks-Taskname'), + 'json' => $task, + 'task' => $array, + 'name_header' => request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'), ], [ - 'json' => 'required|json', - 'task' => 'required|array', - 'task.data' => 'required|array', + 'json' => 'required|json', + 'task' => 'required|array', + 'task.data' => 'required|array', 'name_header' => 'required|string', ]); @@ -119,12 +120,11 @@ private function handleTask(array $task): void $this->loadQueueRetryConfig($job); - $taskName = request()->header('X-Cloudtasks-Taskname'); $fullTaskName = $this->client->taskName( $this->config['project'], $this->config['location'], $job->getQueue() ?: $this->config['queue'], - $taskName, + request()->header('X-CloudTasks-TaskName') ?? request()->header('X-AppEngine-TaskName'), ); try { @@ -140,9 +140,9 @@ private function handleTask(array $task): void // If the task has a [X-CloudTasks-TaskRetryCount] header higher than 0, then // we know the job was created using an earlier version of the package. This // job does not have the attempts tracked internally yet. - $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount'); - if ($taskRetryCountHeader && (int) $taskRetryCountHeader > 0) { - $job->setAttempts((int) $taskRetryCountHeader); + $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount') ?? request()->header('X-AppEngine-TaskRetryCount'); + if ($taskRetryCountHeader && (int)$taskRetryCountHeader > 0) { + $job->setAttempts((int)$taskRetryCountHeader); } else { $job->setAttempts($task['internal']['attempts']); } @@ -173,11 +173,14 @@ private function loadQueueRetryConfig(CloudTasksJob $job): void public static function getCommandProperties(string $command): array { if (Str::startsWith($command, 'O:')) { - return (array) unserialize($command, ['allowed_classes' => false]); + return (array)unserialize($command, ['allowed_classes' => false]); } if (app()->bound(Encrypter::class)) { - return (array) unserialize(app(Encrypter::class)->decrypt($command), ['allowed_classes' => ['Illuminate\Support\Carbon']]); + return (array)unserialize( + app(Encrypter::class)->decrypt($command), + ['allowed_classes' => ['Illuminate\Support\Carbon']] + ); } return []; diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index 6cb7b99..c8820c9 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -241,10 +241,15 @@ public function it_returns_info_about_a_specific_task() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard() + public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + CloudTasksApi::fake(); $tasksBefore = StackkitCloudTask::count(); $job = $this->dispatch(new SimpleJob()); @@ -280,10 +285,15 @@ public function when_dashboard_is_disabled_jobs_will_not_be_added_to_the_dashboa /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_scheduled_it_will_be_added_as_such() + public function when_a_job_is_scheduled_it_will_be_added_as_such(array $test) { // Arrange + $this->withTaskType($test['task_type']); + CloudTasksApi::fake(); Carbon::setTestNow(now()); $tasksBefore = StackkitCloudTask::count(); @@ -305,10 +315,15 @@ public function when_a_job_is_scheduled_it_will_be_added_as_such() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_running_it_will_be_updated_in_the_dashboard() + public function when_a_job_is_running_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -331,10 +346,15 @@ public function when_a_job_is_running_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_successful_it_will_be_updated_in_the_dashboard() + public function when_a_job_is_successful_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -357,10 +377,15 @@ public function when_a_job_is_successful_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_errors_it_will_be_updated_in_the_dashboard() + public function when_a_job_errors_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -384,10 +409,15 @@ public function when_a_job_errors_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_fails_it_will_be_updated_in_the_dashboard() + public function when_a_job_fails_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -416,10 +446,15 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() + public function when_a_job_is_released_it_will_be_updated_in_the_dashboard(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); @@ -447,10 +482,15 @@ public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() /** * @test + * + * @testWith [{"task_type": "http"}] + * [{"task_type": "appengine"}] */ - public function job_release_delay_is_added_to_the_metadata() + public function job_release_delay_is_added_to_the_metadata(array $test) { // Arrange + $this->withTaskType($test['task_type']); + \Illuminate\Support\Carbon::setTestNow(now()); CloudTasksApi::fake(); OpenIdVerificator::fake(); diff --git a/tests/ConfigHandlerTest.php b/tests/ConfigHandlerTest.php index 523f6a4..6c30e3c 100644 --- a/tests/ConfigHandlerTest.php +++ b/tests/ConfigHandlerTest.php @@ -14,7 +14,7 @@ public function test_it_allows_a_handler_url_to_contain_path(string $handler, st self::assertSame($expectedHandler, Config::getHandler($handler)); } - public function handlerDataProvider(): array + public static function handlerDataProvider(): array { return [ ['https://example.com', 'https://example.com/handle-task'], diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php deleted file mode 100644 index 317a801..0000000 --- a/tests/ConfigTest.php +++ /dev/null @@ -1,54 +0,0 @@ -setConfigValue('project', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidProject()); - - SimpleJob::dispatch(); - } - - /** @test */ - public function location_is_required() - { - $this->setConfigValue('location', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidLocation()); - - SimpleJob::dispatch(); - } - - /** @test */ - public function there_is_a_sensible_handler_default() - { - $this->setConfigValue('handler', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidHandler()); - - SimpleJob::dispatch(); - } - - /** @test */ - public function service_email_is_required() - { - $this->setConfigValue('service_account_email', ''); - - $this->expectException(Error::class); - $this->expectExceptionMessage(Errors::invalidServiceAccountEmail()); - - SimpleJob::dispatch(); - } -} diff --git a/tests/QueueAppEngineTest.php b/tests/QueueAppEngineTest.php new file mode 100644 index 0000000..994444f --- /dev/null +++ b/tests/QueueAppEngineTest.php @@ -0,0 +1,74 @@ +withTaskType('appengine'); + } + + /** + * @test + */ + public function an_app_engine_http_request_with_the_handler_url_is_made() + { + // Arrange + CloudTasksApi::fake(); + + // Act + $this->dispatch(new SimpleJob()); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task): bool { + return $task->getAppEngineHttpRequest()->getRelativeUri() === '/handle-task'; + }); + } + + /** + * @test + */ + public function it_routes_to_the_service() + { + // Arrange + CloudTasksApi::fake(); + + // Act + $this->dispatch(new SimpleJob()); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task): bool { + return $task->getAppEngineHttpRequest()->getAppEngineRouting()->getService() === 'api'; + }); + } + + /** + * @test + */ + public function it_contains_the_payload() + { + // Arrange + CloudTasksApi::fake(); + + // Act + $this->dispatch($job = new SimpleJob()); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) use ($job): bool { + $decoded = json_decode($task->getAppEngineHttpRequest()->getBody(), true); + + return $decoded['displayName'] === SimpleJob::class + && $decoded['job'] === 'Illuminate\Queue\CallQueuedHandler@call' + && $decoded['data']['command'] === serialize($job); + }); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index bbdd1e1..048d6a9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -130,12 +130,20 @@ public function dispatch($job) $task = null; Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$payloadAsArray, &$task) { - $payload = $event->task->getHttpRequest()->getBody(); + $request = $event->task->getHttpRequest() ?? $event->task->getAppEngineHttpRequest(); + $payload = $request->getBody(); $payloadAsArray = json_decode($payload, true); $task = $event->task; [,,,,,,,$taskName] = explode('/', $task->getName()); - request()->headers->set('X-Cloudtasks-Taskname', $taskName); + + if ($task->hasHttpRequest()) { + request()->headers->set('X-Cloudtasks-Taskname', $taskName); + } + + if ($task->hasAppEngineHttpRequest()) { + request()->headers->set('X-AppEngine-TaskName', $taskName); + } }); dispatch($job); @@ -248,4 +256,26 @@ public function getJobReleasedAfterExceptionEvent(): string ? PackageJobReleasedAfterException::class : JobReleasedAfterException::class; } + + public function withTaskType(string $taskType): void + { + switch ($taskType) { + case 'appengine': + $this->setConfigValue('handler', null); + $this->setConfigValue('service_account_email', null); + $this->setConfigValue('signed_audience', null); + + $this->setConfigValue('app_engine', true); + $this->setConfigValue('app_engine_service', 'api'); + break; + case 'http': + $this->setConfigValue('app_engine', false); + $this->setConfigValue('app_engine_service', null); + + $this->setConfigValue('handler', 'https://docker.for.mac.localhost:8080'); + $this->setConfigValue('service_account_email', 'info@stackkit.io'); + $this->setConfigValue('signed_audience', true); + break; + } + } }