Skip to content

Commit 28c5729

Browse files
committed
Add test coverage for creating tasks
1 parent b9a451b commit 28c5729

File tree

8 files changed

+243
-5
lines changed

8 files changed

+243
-5
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Pterodactyl\Exceptions\Service;
4+
5+
use Throwable;
6+
use Pterodactyl\Exceptions\DisplayException;
7+
8+
class ServiceLimitExceededException extends DisplayException
9+
{
10+
/**
11+
* Exception thrown when something goes over a defined limit, such as allocated
12+
* ports, tasks, databases, etc.
13+
*
14+
* @param string $message
15+
* @param \Throwable|null $previous
16+
*/
17+
public function __construct(string $message, Throwable $previous = null)
18+
{
19+
parent::__construct($message, $previous, self::LEVEL_WARNING);
20+
}
21+
}

app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
1414
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
1515
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
16+
use Pterodactyl\Exceptions\Service\ServiceLimitExceededException;
1617
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
1718
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
1819

@@ -44,11 +45,15 @@ public function __construct(TaskRepository $repository)
4445
* @return array
4546
*
4647
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
48+
* @throws \Pterodactyl\Exceptions\Service\ServiceLimitExceededException
4749
*/
4850
public function store(StoreTaskRequest $request, Server $server, Schedule $schedule)
4951
{
50-
if ($schedule->server_id !== $server->id) {
51-
throw new NotFoundHttpException;
52+
$limit = config('pterodactyl.client_features.schedules.per_schedule_task_limit', 10);
53+
if ($schedule->tasks()->count() >= $limit) {
54+
throw new ServiceLimitExceededException(
55+
"Schedules may not have more than {$limit} tasks associated with them. Creating this task would put this schedule over the limit."
56+
);
5257
}
5358

5459
$lastTask = $schedule->tasks->last();

config/pterodactyl.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@
162162
'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true),
163163
'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true),
164164
],
165+
166+
'schedules' => [
167+
// The total number of tasks that can exist for any given schedule at once.
168+
'per_schedule_task_limit' => 10,
169+
],
165170
],
166171

167172
/*

tests/Integration/Api/Client/ClientApiIntegrationTestCase.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
use ReflectionClass;
77
use Carbon\CarbonImmutable;
88
use Pterodactyl\Models\Node;
9+
use Pterodactyl\Models\Task;
910
use Pterodactyl\Models\User;
11+
use Webmozart\Assert\Assert;
1012
use Pterodactyl\Models\Model;
1113
use Pterodactyl\Models\Server;
1214
use Pterodactyl\Models\Subuser;
1315
use Pterodactyl\Models\Location;
16+
use Pterodactyl\Models\Schedule;
1417
use Illuminate\Support\Collection;
1518
use Pterodactyl\Tests\Integration\IntegrationTestCase;
1619
use Pterodactyl\Transformers\Api\Client\BaseClientTransformer;
@@ -41,6 +44,33 @@ public function setUp(): void
4144
CarbonImmutable::setTestNow(Carbon::now());
4245
}
4346

47+
/**
48+
* Returns a link to the specific resource using the client API.
49+
*
50+
* @param mixed $model
51+
* @param string|null $append
52+
* @return string
53+
*/
54+
protected function link($model, $append = null): string
55+
{
56+
Assert::isInstanceOfAny($model, [Server::class, Schedule::class, Task::class]);
57+
58+
$link = '';
59+
switch (get_class($model)) {
60+
case Server::class:
61+
$link = "/api/client/servers/{$model->uuid}";
62+
break;
63+
case Schedule::class:
64+
$link = "/api/client/servers/{$model->server->uuid}/schedules/{$model->id}";
65+
break;
66+
case Task::class:
67+
$link = "/api/client/servers/{$model->schedule->server->uuid}/schedules/{$model->schedule->id}/tasks/{$model->id}";
68+
break;
69+
}
70+
71+
return $link . ($append ? '/' . ltrim($append, '/') : '');
72+
}
73+
4474
/**
4575
* Generates a user and a server for that user. If an array of permissions is passed it
4676
* is assumed that the user is actually a subuser of the server.

tests/Integration/Api/Client/Server/Schedule/DeleteServerScheduleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function testNotFoundErrorIsReturnedIfScheduleDoesNotExistAtAll()
5050
public function testNotFoundErrorIsReturnedIfScheduleDoesNotBelongToServer()
5151
{
5252
[$user, $server] = $this->generateTestAccount();
53-
[, $server2] = $this->generateTestAccount();
53+
[, $server2] = $this->generateTestAccount(['user_id' => $user->id]);
5454

5555
$schedule = factory(Schedule::class)->create(['server_id' => $server2->id]);
5656

tests/Integration/Api/Client/Server/Schedule/GetServerSchedulesTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function testServerSchedulesAreReturned($permissions, $individual)
6464
public function testScheduleBelongingToAnotherServerCannotBeViewed()
6565
{
6666
[$user, $server] = $this->generateTestAccount();
67-
[, $server2] = $this->generateTestAccount();
67+
[, $server2] = $this->generateTestAccount(['user_id' => $user->id]);
6868

6969
$schedule = factory(Schedule::class)->create(['server_id' => $server2->id]);
7070

tests/Integration/Api/Client/Server/Schedule/UpdateServerScheduleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function testScheduleCanBeUpdated($permissions)
5050
public function testErrorIsReturnedIfScheduleDoesNotBelongToServer()
5151
{
5252
[$user, $server] = $this->generateTestAccount();
53-
[, $server2] = $this->generateTestAccount();
53+
[, $server2] = $this->generateTestAccount(['user_id' => $user->id]);
5454

5555
$schedule = factory(Schedule::class)->create(['server_id' => $server2->id]);
5656

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server\ScheduleTask;
4+
5+
use Pterodactyl\Models\Task;
6+
use Illuminate\Http\Response;
7+
use Pterodactyl\Models\Schedule;
8+
use Pterodactyl\Models\Permission;
9+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
10+
11+
class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase
12+
{
13+
/**
14+
* Test that a task can be created.
15+
*
16+
* @param array $permissions
17+
* @dataProvider permissionsDataProvider
18+
*/
19+
public function testTaskCanBeCreated($permissions)
20+
{
21+
[$user, $server] = $this->generateTestAccount($permissions);
22+
23+
/** @var \Pterodactyl\Models\Schedule $schedule */
24+
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
25+
$this->assertEmpty($schedule->tasks);
26+
27+
$response = $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
28+
'action' => 'command',
29+
'payload' => 'say Test',
30+
'time_offset' => 10,
31+
'sequence_id' => 1,
32+
]);
33+
34+
$response->assertOk();
35+
/** @var \Pterodactyl\Models\Task $task */
36+
$task = Task::query()->findOrFail($response->json('attributes.id'));
37+
38+
$this->assertSame($schedule->id, $task->schedule_id);
39+
$this->assertSame(1, $task->sequence_id);
40+
$this->assertSame('command', $task->action);
41+
$this->assertSame('say Test', $task->payload);
42+
$this->assertSame(10, $task->time_offset);
43+
$this->assertJsonTransformedWith($response->json('attributes'), $task);
44+
}
45+
46+
/**
47+
* Test that validation errors are returned correctly if bad data is passed into the API.
48+
*/
49+
public function testValidationErrorsAreReturned()
50+
{
51+
[$user, $server] = $this->generateTestAccount();
52+
53+
/** @var \Pterodactyl\Models\Schedule $schedule */
54+
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
55+
56+
$response = $this->actingAs($user)->postJson($this->link($schedule, '/tasks'))->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
57+
58+
foreach (['action', 'payload', 'time_offset'] as $i => $field) {
59+
$response->assertJsonPath("errors.{$i}.code", $field === 'payload' ? 'required_unless' : 'required');
60+
$response->assertJsonPath("errors.{$i}.source.field", $field);
61+
}
62+
63+
$this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
64+
'action' => 'hodor',
65+
'payload' => 'say Test',
66+
'time_offset' => 0,
67+
])
68+
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
69+
->assertJsonPath('errors.0.code', 'in')
70+
->assertJsonPath('errors.0.source.field', 'action');
71+
72+
$this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
73+
'action' => 'command',
74+
'time_offset' => 0,
75+
])
76+
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
77+
->assertJsonPath('errors.0.code', 'required_unless')
78+
->assertJsonPath('errors.0.source.field', 'payload');
79+
80+
$this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
81+
'action' => 'command',
82+
'payload' => 'say Test',
83+
'time_offset' => 0,
84+
'sequence_id' => 'hodor',
85+
])
86+
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
87+
->assertJsonPath('errors.0.code', 'numeric')
88+
->assertJsonPath('errors.0.source.field', 'sequence_id');
89+
}
90+
91+
/**
92+
* Test that backups can be tasked out correctly since they do not require a payload.
93+
*/
94+
public function testBackupsCanBeTaskedCorrectly()
95+
{
96+
[$user, $server] = $this->generateTestAccount();
97+
98+
/** @var \Pterodactyl\Models\Schedule $schedule */
99+
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
100+
101+
$this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
102+
'action' => 'backup',
103+
'time_offset' => 0,
104+
])->assertOk();
105+
106+
$this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
107+
'action' => 'backup',
108+
'payload' => "file.txt\nfile2.log",
109+
'time_offset' => 0,
110+
])->assertOk();
111+
}
112+
113+
/**
114+
* Test that an error is returned if the user attempts to create an additional task that
115+
* would put the schedule over the task limit.
116+
*/
117+
public function testErrorIsReturnedIfTooManyTasksExistForSchedule()
118+
{
119+
config()->set('pterodactyl.client_features.schedules.per_schedule_task_limit', 2);
120+
121+
[$user, $server] = $this->generateTestAccount();
122+
123+
/** @var \Pterodactyl\Models\Schedule $schedule */
124+
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
125+
factory(Task::class)->times(2)->create(['schedule_id' => $schedule->id]);
126+
127+
$this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [
128+
'action' => 'command',
129+
'payload' => 'say test',
130+
'time_offset' => 0,
131+
])
132+
->assertStatus(Response::HTTP_BAD_REQUEST)
133+
->assertJsonPath('errors.0.code', 'ServiceLimitExceededException')
134+
->assertJsonPath('errors.0.detail', 'Schedules may not have more than 2 tasks associated with them. Creating this task would put this schedule over the limit.');
135+
}
136+
137+
/**
138+
* Test that an error is returned if the targeted schedule does not belong to the server
139+
* in the request.
140+
*/
141+
public function testErrorIsReturnedIfScheduleDoesNotBelongToServer()
142+
{
143+
[$user, $server] = $this->generateTestAccount();
144+
[, $server2] = $this->generateTestAccount(['user_id' => $user->id]);
145+
146+
/** @var \Pterodactyl\Models\Schedule $schedule */
147+
$schedule = factory(Schedule::class)->create(['server_id' => $server2->id]);
148+
149+
$this->actingAs($user)
150+
->postJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}/tasks")
151+
->assertNotFound();
152+
}
153+
154+
/**
155+
* Test that an error is returned if the subuser making the request does not have permission
156+
* to update a schedule.
157+
*/
158+
public function testErrorIsReturnedIfSubuserDoesNotHaveScheduleUpdatePermissions()
159+
{
160+
[$user, $server] = $this->generateTestAccount([Permission::ACTION_SCHEDULE_CREATE]);
161+
162+
/** @var \Pterodactyl\Models\Schedule $schedule */
163+
$schedule = factory(Schedule::class)->create(['server_id' => $server->id]);
164+
165+
$this->actingAs($user)
166+
->postJson($this->link($schedule, '/tasks'))
167+
->assertForbidden();
168+
}
169+
170+
/**
171+
* @return array
172+
*/
173+
public function permissionsDataProvider(): array
174+
{
175+
return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]];
176+
}
177+
}

0 commit comments

Comments
 (0)