Skip to content

Commit 2a85fd4

Browse files
committed
Add request validation to TaskHandler
1 parent e22e15b commit 2a85fd4

File tree

4 files changed

+178
-35
lines changed

4 files changed

+178
-35
lines changed

src/TaskHandler.php

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Illuminate\Queue\Jobs\Job;
1010
use Illuminate\Queue\WorkerOptions;
1111
use Illuminate\Support\Str;
12+
use Illuminate\Validation\ValidationException;
13+
use Safe\Exceptions\JsonException;
1214
use stdClass;
1315
use UnexpectedValueException;
1416
use function Safe\json_decode;
@@ -40,9 +42,9 @@ public function __construct(CloudTasksClient $client)
4042
$this->client = $client;
4143
}
4244

43-
public function handle(?array $task = null): void
45+
public function handle(?string $task = null): void
4446
{
45-
$task = $task ?: $this->captureTask();
47+
$task = $this->captureTask($task);
4648

4749
$this->loadQueueConnectionConfiguration($task);
4850

@@ -53,6 +55,47 @@ public function handle(?array $task = null): void
5355
$this->handleTask($task);
5456
}
5557

58+
/**
59+
* @param string|array|null $task
60+
* @return array
61+
* @throws JsonException
62+
*/
63+
private function captureTask($task): array
64+
{
65+
$task = $task ?: (string) (request()->getContent());
66+
67+
try {
68+
$array = json_decode($task, true);
69+
} catch (JsonException $e) {
70+
$array = [];
71+
}
72+
73+
$validator = validator([
74+
'json' => $task,
75+
'task' => $array,
76+
'name_header' => request()->header('X-CloudTasks-Taskname'),
77+
'retry_count_header' => request()->header('X-CloudTasks-TaskRetryCount'),
78+
], [
79+
'json' => 'required|json',
80+
'task' => 'required|array',
81+
'task.data' => 'required|array',
82+
'name_header' => 'required|string',
83+
'retry_count_header' => 'required|numeric',
84+
]);
85+
86+
try {
87+
$validator->validate();
88+
} catch (ValidationException $e) {
89+
if (config('app.debug')) {
90+
throw $e;
91+
} else {
92+
abort(404);
93+
}
94+
}
95+
96+
return json_decode($task, true);
97+
}
98+
5699
private function loadQueueConnectionConfiguration(array $task): void
57100
{
58101
/**
@@ -71,26 +114,6 @@ private function setQueue(): void
71114
$this->queue = new CloudTasksQueue($this->config, $this->client);
72115
}
73116

74-
/**
75-
* @throws CloudTasksException
76-
*/
77-
private function captureTask(): array
78-
{
79-
$input = (string) (request()->getContent());
80-
81-
if (!$input) {
82-
throw new CloudTasksException('Could not read incoming task');
83-
}
84-
85-
$task = json_decode($input, true);
86-
87-
if (!is_array($task)) {
88-
throw new CloudTasksException('Could not decode incoming task');
89-
}
90-
91-
return $task;
92-
}
93-
94117
private function handleTask(array $task): void
95118
{
96119
$job = new CloudTasksJob($task, $this->queue);

tests/CloudTasksDashboardTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard()
259259
'name' => SimpleJob::class,
260260
]);
261261
$payload = \Safe\json_decode($task->getMetadata()['payload'], true);
262-
$this->assertSame($payload, $job->payload);
262+
$this->assertSame($payload, $job->payloadAsArray);
263263
}
264264

265265
/**

tests/TaskHandlerTest.php

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
use Illuminate\Queue\Events\JobProcessing;
1010
use Illuminate\Support\Facades\Event;
1111
use Illuminate\Support\Facades\Log;
12+
use Illuminate\Validation\ValidationException;
1213
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi;
1314
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException;
1415
use Stackkit\LaravelGoogleCloudTasksQueue\LogFake;
1516
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
1617
use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask;
18+
use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
19+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
1720
use Tests\Support\EncryptedJob;
1821
use Tests\Support\FailingJob;
1922
use Tests\Support\SimpleJob;
@@ -28,6 +31,118 @@ protected function setUp(): void
2831
CloudTasksApi::fake();
2932
}
3033

34+
/**
35+
* @test
36+
* @testWith [true]
37+
* [false]
38+
*/
39+
public function it_returns_responses_for_empty_payloads($debug)
40+
{
41+
// Arrange
42+
config()->set('app.debug', $debug);
43+
44+
// Act
45+
$response = $this->postJson(action([TaskHandler::class, 'handle']));
46+
47+
// Assert
48+
if ($debug) {
49+
$response->assertJsonValidationErrorFor('task');
50+
} else {
51+
$response->assertNotFound();
52+
}
53+
}
54+
55+
/**
56+
* @test
57+
* @testWith [true]
58+
* [false]
59+
*/
60+
public function it_returns_responses_for_invalid_json($debug)
61+
{
62+
// Arrange
63+
config()->set('app.debug', $debug);
64+
65+
// Act
66+
$response = $this->call(
67+
'POST',
68+
action([TaskHandler::class, 'handle']),
69+
[],
70+
[],
71+
[],
72+
[
73+
'HTTP_ACCEPT' => 'application/json',
74+
],
75+
'test',
76+
);
77+
78+
// Assert
79+
if ($debug) {
80+
$response->assertJsonValidationErrorFor('task');
81+
$this->assertEquals('The json must be a valid JSON string.', $response->json('errors.json.0'));
82+
} else {
83+
$response->assertNotFound();
84+
}
85+
}
86+
87+
/**
88+
* @test
89+
* @testWith ["{\"invalid\": \"data\"}", "The task.data field is required."]
90+
* ["{\"data\": \"\"}", "The task.data field is required."]
91+
* ["{\"data\": \"test\"}", "The task.data must be an array."]
92+
*/
93+
public function it_returns_responses_for_invalid_payloads(string $payload, string $expectedMessage)
94+
{
95+
// Arrange
96+
97+
// Act
98+
$response = $this->call(
99+
'POST',
100+
action([TaskHandler::class, 'handle']),
101+
[],
102+
[],
103+
[],
104+
[
105+
'HTTP_ACCEPT' => 'application/json',
106+
],
107+
$payload,
108+
);
109+
110+
// Assert
111+
$response->assertJsonValidationErrorFor('task.data');
112+
$this->assertEquals($expectedMessage, $response->json(['errors', 'task.data', 0]));
113+
}
114+
115+
/**
116+
* @test
117+
* @testWith [true]
118+
* [false]
119+
*/
120+
public function it_validates_headers(bool $withHeaders)
121+
{
122+
// Arrange
123+
$this->withExceptionHandling();
124+
125+
// Act
126+
$response = $this->postJson(
127+
action([TaskHandler::class, 'handle']),
128+
[],
129+
$withHeaders
130+
? [
131+
'X-CloudTasks-Taskname' => 'MyTask',
132+
'X-CloudTasks-TaskRetryCount' => 0,
133+
] : []
134+
);
135+
136+
// Assert
137+
if ($withHeaders) {
138+
$response->assertJsonMissingValidationErrors('name_header');
139+
$response->assertJsonMissingValidationErrors('retry_count_header');
140+
} else {
141+
$response->assertJsonValidationErrors('name_header');
142+
$response->assertJsonValidationErrors('retry_count_header');
143+
}
144+
}
145+
31146
/**
32147
* @test
33148
*/
@@ -245,7 +360,7 @@ public function test_max_attempts_in_combination_with_retry_until()
245360
$job->run();
246361

247362
# After 2 attempts both Laravel versions should report the same: 2 errors and 0 failures.
248-
$task = StackkitCloudTask::whereTaskUuid($job->payload['uuid'])->firstOrFail();
363+
$task = StackkitCloudTask::whereTaskUuid($job->payloadAsArray['uuid'])->firstOrFail();
249364
$this->assertEquals(2, $task->getNumberOfAttempts());
250365
$this->assertEquals('error', $task->status);
251366

@@ -288,7 +403,7 @@ public function it_can_handle_encrypted_jobs()
288403
// Assert
289404
$this->assertStringContainsString(
290405
'O:26:"Tests\Support\EncryptedJob"',
291-
decrypt($job->payload['data']['command']),
406+
decrypt($job->payloadAsArray['data']['command']),
292407
);
293408

294409
Log::assertLogged('EncryptedJob:success');

tests/TestCase.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,43 +116,48 @@ protected function setConfigValue($key, $value)
116116
public function dispatch($job)
117117
{
118118
$payload = null;
119+
$payloadAsArray = [];
119120
$task = null;
120121

121-
Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$task) {
122-
$payload = json_decode($event->task->getHttpRequest()->getBody(), true);
122+
Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$payloadAsArray, &$task) {
123+
$payload = $event->task->getHttpRequest()->getBody();
124+
$payloadAsArray = json_decode($payload, true);
123125
$task = $event->task;
124126

125127
request()->headers->set('X-Cloudtasks-Taskname', $task->getName());
126128
});
127129

128130
dispatch($job);
129131

130-
return new class($payload, $task) {
131-
public array $payload = [];
132+
return new class($payload, $payloadAsArray, $task) {
133+
public string $payload;
134+
public array $payloadAsArray;
132135
public Task $task;
133136

134-
public function __construct(array $payload, Task $task)
137+
public function __construct(string $payload, array $payloadAsArray, Task $task)
135138
{
136139
$this->payload = $payload;
140+
$this->payloadAsArray = $payloadAsArray;
137141
$this->task = $task;
138142
}
139143

140144
public function run(): void
141145
{
146+
$taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', -1);
147+
request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1);
148+
142149
rescue(function (): void {
143150
app(TaskHandler::class)->handle($this->payload);
144151
});
145-
146-
$taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', 0);
147-
request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1);
148152
}
149153

150154
public function runWithoutExceptionHandler(): void
151155
{
156+
$taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', -1);
157+
request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1);
158+
152159
app(TaskHandler::class)->handle($this->payload);
153160

154-
$taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', 0);
155-
request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1);
156161
}
157162
};
158163
}

0 commit comments

Comments
 (0)