From e8ea218f2097109cb977dceefdc07bcec460c200 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 4 Mar 2018 22:35:57 -0600 Subject: [PATCH] Add integration test for remaining application api user endpoints --- .../Api/Application/UserTransformer.php | 2 + database/factories/ModelFactory.php | 5 +- .../Location/LocationControllerTest.php | 23 +- .../Users/ExternalUserControllerTest.php | 13 +- .../Application/Users/UserControllerTest.php | 327 ++++++++++++++++++ .../Http/IntegrationJsonRequestAssertions.php | 5 +- 6 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 tests/Integration/Api/Application/Users/UserControllerTest.php diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index 56749deaa..2e55bbb1b 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -53,6 +53,8 @@ class UserTransformer extends BaseTransformer * * @param \Pterodactyl\Models\User $user * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeServers(User $user) { diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 785588ebd..8866fa555 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -1,5 +1,6 @@ define(Pterodactyl\Models\User::class, function (Faker $faker) { 'language' => 'en', 'root_admin' => false, 'use_totp' => false, + 'created_at' => Chronos::now(), + 'updated_at' => Chronos::now(), ]; }); @@ -69,7 +72,7 @@ $factory->state(Pterodactyl\Models\User::class, 'admin', function () { $factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), - 'short' => $faker->domainWord, + 'short' => $faker->unique()->domainWord, 'long' => $faker->catchPhrase, ]; }); diff --git a/tests/Integration/Api/Application/Location/LocationControllerTest.php b/tests/Integration/Api/Application/Location/LocationControllerTest.php index db315211b..b43f745d7 100644 --- a/tests/Integration/Api/Application/Location/LocationControllerTest.php +++ b/tests/Integration/Api/Application/Location/LocationControllerTest.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Tests\Integration\Api\Application\Location; use Pterodactyl\Models\Node; +use Illuminate\Http\Response; use Pterodactyl\Models\Location; use Pterodactyl\Transformers\Api\Application\NodeTransformer; use Pterodactyl\Transformers\Api\Application\ServerTransformer; @@ -17,8 +18,8 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase { $locations = factory(Location::class)->times(2)->create(); - $response = $this->json('GET', '/api/application/locations'); - $response->assertStatus(200); + $response = $this->getJson('/api/application/locations'); + $response->assertStatus(Response::HTTP_OK); $response->assertJsonCount(2, 'data'); $response->assertJsonStructure([ 'object', @@ -71,8 +72,8 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase { $location = factory(Location::class)->create(); - $response = $this->json('GET', '/api/application/locations/' . $location->id); - $response->assertStatus(200); + $response = $this->getJson('/api/application/locations/' . $location->id); + $response->assertStatus(Response::HTTP_OK); $response->assertJsonCount(2); $response->assertJsonStructure(['object', 'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at']]); $response->assertJson([ @@ -95,8 +96,8 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase $location = factory(Location::class)->create(); $server = $this->createServerModel(['user_id' => $this->getApiUser()->id, 'location_id' => $location->id]); - $response = $this->json('GET', '/api/application/locations/' . $location->id . '?include=servers,nodes'); - $response->assertStatus(200); + $response = $this->getJson('/api/application/locations/' . $location->id . '?include=servers,nodes'); + $response->assertStatus(Response::HTTP_OK); $response->assertJsonCount(2)->assertJsonCount(2, 'attributes.relationships'); $response->assertJsonStructure([ 'attributes' => [ @@ -145,8 +146,8 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase $location = factory(Location::class)->create(); factory(Node::class)->create(['location_id' => $location->id]); - $response = $this->json('GET', '/api/application/locations/' . $location->id . '?include=nodes'); - $response->assertStatus(200); + $response = $this->getJson('/api/application/locations/' . $location->id . '?include=nodes'); + $response->assertStatus(Response::HTTP_OK); $response->assertJsonCount(2)->assertJsonCount(1, 'attributes.relationships'); $response->assertJsonStructure([ 'attributes' => [ @@ -176,7 +177,7 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase */ public function testGetMissingLocation() { - $response = $this->json('GET', '/api/application/locations/nil'); + $response = $this->getJson('/api/application/locations/nil'); $this->assertNotFoundJson($response); } @@ -189,7 +190,7 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase $location = factory(Location::class)->create(); $this->createNewDefaultApiKey($this->getApiUser(), ['r_locations' => 0]); - $response = $this->json('GET', '/api/application/locations/' . $location->id); + $response = $this->getJson('/api/application/locations/' . $location->id); $this->assertAccessDeniedJson($response); } @@ -201,7 +202,7 @@ class LocationControllerTest extends ApplicationApiIntegrationTestCase { $this->createNewDefaultApiKey($this->getApiUser(), ['r_locations' => 0]); - $response = $this->json('GET', '/api/application/locations/nil'); + $response = $this->getJson('/api/application/locations/nil'); $this->assertAccessDeniedJson($response); } } diff --git a/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php b/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php index f4c7153b2..f28e15a93 100644 --- a/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php +++ b/tests/Integration/Api/Application/Users/ExternalUserControllerTest.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Tests\Integration\Api\Application\Users; use Pterodactyl\Models\User; +use Illuminate\Http\Response; use Pterodactyl\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase; class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase @@ -14,8 +15,8 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase { $user = factory(User::class)->create(); - $response = $this->json('GET', '/api/application/users/external/' . $user->external_id); - $response->assertStatus(200); + $response = $this->getJson('/api/application/users/external/' . $user->external_id); + $response->assertStatus(Response::HTTP_OK); $response->assertJsonCount(2); $response->assertJsonStructure([ 'object', @@ -47,9 +48,9 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase /** * Test that an invalid external ID returns a 404 error. */ - public function testGetMissingLocation() + public function testGetMissingUser() { - $response = $this->json('GET', '/api/application/users/external/nil'); + $response = $this->getJson('/api/application/users/external/nil'); $this->assertNotFoundJson($response); } @@ -62,7 +63,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase $user = factory(User::class)->create(); $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); - $response = $this->json('GET', '/api/application/users/external/' . $user->external_id); + $response = $this->getJson('/api/application/users/external/' . $user->external_id); $this->assertAccessDeniedJson($response); } @@ -74,7 +75,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase { $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); - $response = $this->json('GET', '/api/application/users/external/nil'); + $response = $this->getJson('/api/application/users/external/nil'); $this->assertAccessDeniedJson($response); } } diff --git a/tests/Integration/Api/Application/Users/UserControllerTest.php b/tests/Integration/Api/Application/Users/UserControllerTest.php new file mode 100644 index 000000000..704457e4e --- /dev/null +++ b/tests/Integration/Api/Application/Users/UserControllerTest.php @@ -0,0 +1,327 @@ +create(); + + $response = $this->getJson('/api/application/users'); + $response->assertStatus(Response::HTTP_OK); + $response->assertJsonCount(2, 'data'); + $response->assertJsonStructure([ + 'object', + 'data' => [ + ['object', 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'language', 'root_admin', '2fa', 'created_at', 'updated_at']], + ['object', 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'language', 'root_admin', '2fa', 'created_at', 'updated_at']], + ], + 'meta' => ['pagination' => ['total', 'count', 'per_page', 'current_page', 'total_pages']], + ]); + + $response + ->assertJson([ + 'object' => 'list', + 'data' => [[], []], + 'meta' => [ + 'pagination' => [ + 'total' => 2, + 'count' => 2, + 'per_page' => 50, + 'current_page' => 1, + 'total_pages' => 1, + ], + ], + ]) + ->assertJsonFragment([ + 'object' => 'user', + 'attributes' => [ + 'id' => $this->getApiUser()->id, + 'external_id' => $this->getApiUser()->external_id, + 'uuid' => $this->getApiUser()->uuid, + 'username' => $this->getApiUser()->username, + 'email' => $this->getApiUser()->email, + 'first_name' => $this->getApiUser()->name_first, + 'last_name' => $this->getApiUser()->name_last, + 'language' => $this->getApiUser()->language, + 'root_admin' => (bool) $this->getApiUser()->root_admin, + '2fa' => (bool) $this->getApiUser()->totp_enabled, + 'created_at' => $this->formatTimestamp($this->getApiUser()->created_at), + 'updated_at' => $this->formatTimestamp($this->getApiUser()->updated_at), + ], + ]) + ->assertJsonFragment([ + 'object' => 'user', + 'attributes' => [ + 'id' => $user->id, + 'external_id' => $user->external_id, + 'uuid' => $user->uuid, + 'username' => $user->username, + 'email' => $user->email, + 'first_name' => $user->name_first, + 'last_name' => $user->name_last, + 'language' => $user->language, + 'root_admin' => (bool) $user->root_admin, + '2fa' => (bool) $user->totp_enabled, + 'created_at' => $this->formatTimestamp($user->created_at), + 'updated_at' => $this->formatTimestamp($user->updated_at), + ], + ]); + } + + /** + * Test getting a single user. + */ + public function testGetSingleUser() + { + $user = factory(User::class)->create(); + + $response = $this->getJson('/api/application/users/' . $user->id); + $response->assertStatus(Response::HTTP_OK); + $response->assertJsonCount(2); + $response->assertJsonStructure([ + 'object', + 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'], + ]); + + $response->assertJson([ + 'object' => 'user', + 'attributes' => [ + 'id' => $user->id, + 'external_id' => $user->external_id, + 'uuid' => $user->uuid, + 'username' => $user->username, + 'email' => $user->email, + 'first_name' => $user->name_first, + 'last_name' => $user->name_last, + 'language' => $user->language, + 'root_admin' => (bool) $user->root_admin, + '2fa' => (bool) $user->totp_enabled, + 'created_at' => $this->formatTimestamp($user->created_at), + 'updated_at' => $this->formatTimestamp($user->updated_at), + ], + ]); + } + + /** + * Test that the correct relationships can be loaded. + */ + public function testRelationshipsCanBeLoaded() + { + $user = factory(User::class)->create(); + $server = $this->createServerModel(['user_id' => $user->id]); + + $response = $this->getJson('/api/application/users/' . $user->id . '?include=servers'); + $response->assertStatus(Response::HTTP_OK); + $response->assertJsonCount(2); + $response->assertJsonStructure([ + 'object', + 'attributes' => [ + 'id', 'external_id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'language', 'root_admin', '2fa', 'created_at', 'updated_at', + 'relationships' => ['servers' => ['object', 'data' => [['object', 'attributes' => []]]]], + ], + ]); + + $response->assertJsonFragment([ + 'object' => 'list', + 'data' => [ + [ + 'object' => 'server', + 'attributes' => $this->getTransformer(ServerTransformer::class)->transform($server), + ], + ], + ]); + } + + /** + * Test that attempting to load a relationship that the key does not have permission + * for returns a null object. + */ + public function testKeyWithoutPermissionCannotLoadRelationship() + { + $this->createNewDefaultApiKey($this->getApiUser(), ['r_servers' => 0]); + + $user = factory(User::class)->create(); + $this->createServerModel(['user_id' => $user->id]); + + $response = $this->getJson('/api/application/users/' . $user->id . '?include=servers'); + $response->assertStatus(Response::HTTP_OK); + $response->assertJsonCount(2)->assertJsonCount(1, 'attributes.relationships'); + $response->assertJsonStructure([ + 'attributes' => [ + 'relationships' => [ + 'servers' => ['object', 'attributes'], + ], + ], + ]); + + // Just assert that we see the expected relationship IDs in the response. + $response->assertJson([ + 'attributes' => [ + 'relationships' => [ + 'servers' => [ + 'object' => 'null_resource', + 'attributes' => null, + ], + ], + ], + ]); + } + + /** + * Test that an invalid external ID returns a 404 error. + */ + public function testGetMissingUser() + { + $response = $this->getJson('/api/application/users/nil'); + $this->assertNotFoundJson($response); + } + + /** + * Test that an authentication error occurs if a key does not have permission + * to access a resource. + */ + public function testErrorReturnedIfNoPermission() + { + $user = factory(User::class)->create(); + $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); + + $response = $this->getJson('/api/application/users/' . $user->id); + $this->assertAccessDeniedJson($response); + } + + /** + * Test that a users's existence is not exposed unless an API key has permission + * to access the resource. + */ + public function testResourceIsNotExposedWithoutPermissions() + { + $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => 0]); + + $response = $this->getJson('/api/application/users/nil'); + $this->assertAccessDeniedJson($response); + } + + /** + * Test that a user can be created. + */ + public function testCreateUser() + { + $response = $this->postJson('/api/application/users', [ + 'username' => 'testuser', + 'email' => 'test@example.com', + 'first_name' => 'Test', + 'last_name' => 'User', + ]); + + $response->assertStatus(Response::HTTP_CREATED); + $response->assertJsonCount(3); + $response->assertJsonStructure([ + 'object', + 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'], + 'meta' => ['resource'], + ]); + + $this->assertDatabaseHas('users', ['username' => 'testuser', 'email' => 'test@example.com']); + + $user = User::where('username', 'testuser')->first(); + $response->assertJson([ + 'object' => 'user', + 'attributes' => $this->getTransformer(UserTransformer::class)->transform($user), + 'meta' => [ + 'resource' => route('api.application.users.view', $user->id), + ], + ], true); + } + + /** + * Test that a user can be updated. + */ + public function testUpdateUser() + { + $user = factory(User::class)->create(); + + $response = $this->patchJson('/api/application/users/' . $user->id, [ + 'username' => 'new.test.name', + 'email' => 'new@emailtest.com', + 'first_name' => $user->name_first, + 'last_name' => $user->name_last, + ]); + $response->assertStatus(Response::HTTP_OK); + $response->assertJsonCount(2); + $response->assertJsonStructure([ + 'object', + 'attributes' => ['id', 'external_id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'language', 'root_admin', '2fa', 'created_at', 'updated_at'], + ]); + + $this->assertDatabaseHas('users', ['username' => 'new.test.name', 'email' => 'new@emailtest.com']); + $user = $user->fresh(); + + $response->assertJson([ + 'object' => 'user', + 'attributes' => $this->getTransformer(UserTransformer::class)->transform($user), + ]); + } + + /** + * Test that a user can be deleted from the database. + */ + public function testDeleteUser() + { + $user = factory(User::class)->create(); + $this->assertDatabaseHas('users', ['id' => $user->id]); + + $response = $this->delete('/api/application/users/' . $user->id); + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $this->assertDatabaseMissing('users', ['id' => $user->id]); + } + + /** + * Test that an API key without write permissions cannot create, update, or + * delete a user model. + * + * @param string $method + * @param string $url + * + * @dataProvider userWriteEndpointsDataProvider + */ + public function testApiKeyWithoutWritePermissions(string $method, string $url) + { + $this->createNewDefaultApiKey($this->getApiUser(), ['r_users' => AdminAcl::READ]); + + if (str_contains($url, '{id}')) { + $user = factory(User::class)->create(); + $url = str_replace('{id}', $user->id, $url); + } + + $response = $this->$method($url); + $this->assertAccessDeniedJson($response); + } + + /** + * Endpoints that should return a 403 error when the key does not have write + * permissions for user management. + * + * @return array + */ + public function userWriteEndpointsDataProvider(): array + { + return [ + ['postJson', '/api/application/users'], + ['patchJson', '/api/application/users/{id}'], + ['delete', '/api/application/users/{id}'], + ]; + } +} diff --git a/tests/Traits/Http/IntegrationJsonRequestAssertions.php b/tests/Traits/Http/IntegrationJsonRequestAssertions.php index 4bcae3076..aca9233f0 100644 --- a/tests/Traits/Http/IntegrationJsonRequestAssertions.php +++ b/tests/Traits/Http/IntegrationJsonRequestAssertions.php @@ -2,6 +2,7 @@ namespace Tests\Traits; +use Illuminate\Http\Response; use Illuminate\Foundation\Testing\TestResponse; trait IntegrationJsonRequestAssertions @@ -13,7 +14,7 @@ trait IntegrationJsonRequestAssertions */ public function assertNotFoundJson(TestResponse $response) { - $response->assertStatus(404); + $response->assertStatus(Response::HTTP_NOT_FOUND); $response->assertJsonStructure(['errors' => [['code', 'status', 'detail']]]); $response->assertJsonCount(1, 'errors'); $response->assertJson([ @@ -34,7 +35,7 @@ trait IntegrationJsonRequestAssertions */ public function assertAccessDeniedJson(TestResponse $response) { - $response->assertStatus(403); + $response->assertStatus(Response::HTTP_FORBIDDEN); $response->assertJsonStructure(['errors' => [['code', 'status', 'detail']]]); $response->assertJsonCount(1, 'errors'); $response->assertJson([