diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php new file mode 100644 index 00000000..d323de67 --- /dev/null +++ b/app/Http/Controllers/RoleController.php @@ -0,0 +1,120 @@ +middleware('permission:'.RolesPermissions::CAN_SHOW_ROLES)->only(['index', 'show']); + $this->middleware('permission:'.RolesPermissions::CAN_CREATE_ROLES)->only(['store']); + $this->middleware('permission:'.RolesPermissions::CAN_UPDATE_ROLES)->only(['update']); + $this->middleware('permission:'.RolesPermissions::CAN_DELETE_ROLES)->only(['destroy']); + } + + /** + * Retrieve all roles with their permissions. + * + * @return \App\Http\Responses\ApiSuccessResponse + */ + public function index() + { + $roles = Role::all(); + + // Combine the roles with their permissions with out pivot table. + foreach ($roles as &$role) { + $role['permissions'] = $role->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + } + + return new ApiSuccessResponse($roles->toArray()); + } + + /** + * Store a newly created role in the database. + * + * @param \Illuminate\Http\Request $request + * @return \App\Http\Responses\ApiSuccessResponse + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|unique:roles,name', + 'permissions' => 'present|array', + 'permissions.*' => 'sometimes|int|exists:permissions,id', + ]); + + $role = Role::create(['name' => $validated['name'], 'guard_name' => 'web']); + $role->syncPermissions($validated['permissions']); + + $role['permissions'] = $role->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + + return new ApiSuccessResponse($role->toArray(), Response::HTTP_CREATED); + } + + /** + * Display the specified role. + * + * @param \Spatie\Permission\Models\Role $role + * @return \App\Http\Responses\ApiSuccessResponse + */ + public function show(Role $role) + { + $role['permissions'] = $role->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + return new ApiSuccessResponse($role); + } + + /** + * Update a role. + * + * @param \Illuminate\Http\Request $request + * @param \Spatie\Permission\Models\Role $role + * @return \App\Http\Responses\ApiSuccessResponse + */ + public function update(Request $request, Role $role) + { + $validated = $request->validate([ + 'name' => 'required|string|unique:roles,name,'.$role->id, + 'permissions' => 'present|array', + 'permissions.*' => 'sometimes|int|exists:permissions,id', + ]); + + $role->update(['name' => $validated['name']]); + $role->syncPermissions($validated['permissions']); + + $role['permissions'] = $role->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + + return new ApiSuccessResponse($role); + } + + /** + * Delete a role. + * + * @param \Spatie\Permission\Models\Role $role + * @return \App\Http\Responses\ApiSuccessResponse + */ + public function destroy(Role $role) + { + $role->delete(); + + return new ApiSuccessResponse("Role successfully deleted."); + } +} diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 00000000..fff82d6f --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,11 @@ + + */ +class RoleFactory extends Factory +{ + protected $model = Role::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => implode('-', $this->faker->unique()->words(3)), + 'guard_name' => 'web', + ]; + } +} diff --git a/database/migrations/2023_12_14_193625_add_roles_permissions.php b/database/migrations/2023_12_14_193625_add_roles_permissions.php new file mode 100644 index 00000000..090ec41b --- /dev/null +++ b/database/migrations/2023_12_14_193625_add_roles_permissions.php @@ -0,0 +1,54 @@ +insert([ + [ + 'name' => RolesPermissions::CAN_SHOW_ROLES, + 'guard_name' => 'web', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => RolesPermissions::CAN_CREATE_ROLES, + 'guard_name' => 'web', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => RolesPermissions::CAN_UPDATE_ROLES, + 'guard_name' => 'web', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => RolesPermissions::CAN_DELETE_ROLES, + 'guard_name' => 'web', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('permissions')->whereIn('name', [ + RolesPermissions::CAN_SHOW_ROLES, + RolesPermissions::CAN_CREATE_ROLES, + RolesPermissions::CAN_UPDATE_ROLES, + RolesPermissions::CAN_DELETE_ROLES, + ])->delete(); + } +}; diff --git a/routes/api.php b/routes/api.php index 6015235b..4de97518 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,4 +23,5 @@ Route::group(['prefix' => 'v1'], function () { require __DIR__ . '/api/v1/auth.php'; require __DIR__ . '/api/v1/user.php'; + require __DIR__ . '/api/v1/role.php'; }); diff --git a/routes/api/v1/role.php b/routes/api/v1/role.php new file mode 100644 index 00000000..22adc4d0 --- /dev/null +++ b/routes/api/v1/role.php @@ -0,0 +1,12 @@ + 'auth:sanctum'], function () { + Route::get('/roles', [RoleController::class, 'index'])->name('api.v1.roles.index'); + Route::post('/roles', [RoleController::class, 'store'])->name('api.v1.roles.store'); + Route::get('/roles/{role}', [RoleController::class, 'show'])->name('api.v1.roles.show'); + Route::put('/roles/{role}', [RoleController::class, 'update'])->name('api.v1.roles.update'); + Route::delete('/roles/{role}', [RoleController::class, 'destroy'])->name('api.v1.roles.destroy'); +}); diff --git a/tests/Feature/Http/Controllers/RoleControllerTest.php b/tests/Feature/Http/Controllers/RoleControllerTest.php new file mode 100644 index 00000000..68240039 --- /dev/null +++ b/tests/Feature/Http/Controllers/RoleControllerTest.php @@ -0,0 +1,670 @@ +count(3)->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_SHOW_ROLES + ]), + ); + + $roles = Role::all(); + + // Combine the roles with their permissions with out pivot table. + foreach ($roles as &$role) { + $role['permissions'] = $role->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + } + + $response = $this->getJson('/api/v1/roles'); + + $response->assertStatus(200); + + foreach ($roles as $role) { + $response->assertJsonFragment($role->toArray()); + } + } + + /** + * Test the index method of RoleController without roles permissions. + * + * @group RoleController.index + * @covers ::index + */ + public function test_not_index_roles_without_any_permission(): void + { + Role::factory()->count(3)->create(); + + Sanctum::actingAs( + User::factory()->create(), + ); + + $response = $this->getJson('/api/v1/roles'); + + $response->assertStatus(403); + + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the index method of RoleController with all unauthorized role permissions. + * + * @group RoleController.index + * @covers ::index + */ + public function test_not_index_roles_with_unauthorized_role_permissions(): void + { + Role::factory()->count(3)->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_CREATE_ROLES, + RolesPermissions::CAN_UPDATE_ROLES, + RolesPermissions::CAN_DELETE_ROLES, + ]), + ); + + $response = $this->getJson('/api/v1/roles'); + + $response->assertStatus(403); + + $response->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the store method of RoleController with minimal permissions. + * + * @group RoleController.store + * @covers ::store + */ + public function test_store_roles_with_minimal_permission(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_CREATE_ROLES + ]), + ); + + $data = [ + 'name' => 'Test Role', + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->postJson('/api/v1/roles', $data); + + $resData = [ + 'name' => 'Test Role', + 'guard_name' => 'web', + 'permissions' => [ + [ + 'id' => Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + 'name' => UsersPermissions::CAN_SHOW_USERS, + ], + ], + ]; + + $this->assertDatabaseHas('roles', ['name' => 'Test Role']); + + $response + ->assertStatus(201) + ->assertJsonFragment($resData); + } + + /** + * Test the store method of RoleController with wrong permission id. + * + * @group RoleController.store + * @covers ::store + */ + public function test_not_store_roles_with_wrong_permission_id(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_CREATE_ROLES + ]), + ); + + do { + $randPermId = rand(0, 999); + } while (DB::table('permissions')->where('id', $randPermId)->exists()); + + $data = [ + 'name' => 'Test Role', + 'permissions' => [ + $randPermId, + ], + ]; + + $response = $this->postJson('/api/v1/roles', $data); + + $this->assertDatabaseMissing('roles', ['name' => 'Test Role']); + + $response + ->assertStatus(422) + ->assertJsonFragment(['message' => 'The selected permissions.0 is invalid.']); + } + + /** + * Test the store method of RoleController with duplicate name. + * + * @group RoleController.store + * @covers ::store + */ + public function test_not_store_roles_with_duplicate_name(): void + { + $roleOne = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_CREATE_ROLES + ]), + ); + + $data = [ + 'name' => $roleOne->name, + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->postJson('/api/v1/roles', $data); + + $this->assertDatabaseCount('roles', 1); + + $response + ->assertStatus(422) + ->assertJsonFragment(['message' => 'The name has already been taken.']); + } + + /** + * Test the store method of RoleController with unauthorized role permissions. + * + * @group RoleController.store + * @covers ::store + */ + public function test_not_store_roles_with_unauthorized_role_permissions(): void + { + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_SHOW_ROLES, + RolesPermissions::CAN_UPDATE_ROLES, + RolesPermissions::CAN_DELETE_ROLES, + ]), + ); + + $data = [ + 'name' => 'Test Role', + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->postJson('/api/v1/roles', $data); + + $this->assertDatabaseMissing('roles', ['name' => 'Test Role']); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the store method of RoleController without any permissions. + * + * @group RoleController.store + * @covers ::store + */ + public function test_not_stores_role_without_any_permission(): void + { + Sanctum::actingAs( + User::factory()->create(), + ); + + $data = [ + 'name' => 'Test Role', + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->postJson('/api/v1/roles', $data); + + $this->assertDatabaseMissing('roles', ['name' => 'Test Role']); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the show method of RoleController with minimal permissions. + * + * @group RoleController.show + * @covers ::show + */ + public function test_show_roles_with_minimal_permission(): void + { + Role::factory()->count(3)->create(); + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_SHOW_ROLES + ]), + ); + + $response = $this->getJson('/api/v1/roles/' . $role->id); + + $role['permissions'] = $role->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + + $response + ->assertStatus(200) + ->assertJsonFragment($role->toArray()); + } + + /** + * Test the show method of RoleController with wrong role id. + * + * @group RoleController.show + * @covers ::show + */ + public function test_not_show_roles_with_wrong_role_id(): void + { + Role::factory()->count(3)->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_SHOW_ROLES + ]), + ); + + do { + $randRoleId = rand(0, 999); + } while (DB::table('roles')->where('id', $randRoleId)->exists()); + + $response = $this->getJson('/api/v1/roles/' . $randRoleId); + + $response + ->assertStatus(404) + ->assertJsonFragment(['message' => 'No query results for model [App\\Models\\Role] ' . $randRoleId]); + } + + /** + * Test the show method of RoleController with unauthorized role permissions. + * + * @group RoleController.show + * @covers ::show + */ + public function test_not_show_roles_with_unauthorized_role_permissions(): void + { + Role::factory()->count(3)->create(); + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_CREATE_ROLES, + RolesPermissions::CAN_UPDATE_ROLES, + RolesPermissions::CAN_DELETE_ROLES, + ]), + ); + + $response = $this->getJson('/api/v1/roles/' . $role->id); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the show method of RoleController without any permissions. + * + * @group RoleController.show + * @covers ::show + */ + public function test_not_show_roles_without_any_permission(): void + { + Role::factory()->count(3)->create(); + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create(), + ); + + $response = $this->getJson('/api/v1/roles/' . $role->id); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the update method of RoleController with minimal permissions. + * + * @group RoleController.update + * @covers ::update + */ + public function test_update_roles_with_minimal_permission(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_UPDATE_ROLES + ]), + ); + + $data = [ + 'name' => 'Updated Role', + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->putJson('/api/v1/roles/' . $role->id, $data); + + + + $this->assertDatabaseHas('roles', ['id'=> $role->id, 'name' => 'Updated Role']); + + $updatedRole = Role::findById($role->id, 'web'); + $updatedRole['permissions'] = $updatedRole->permissions()->get(['id', 'name'])->makeHidden(['pivot'])->toArray(); + + $response + ->assertStatus(200) + ->assertJsonFragment($updatedRole->toArray()); + } + + /** + * Test the update method of RoleController without permissions array. + * + * @group RoleController.update + * @covers ::update + */ + public function test_not_update_roles_without_permissions_array(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_UPDATE_ROLES + ]), + ); + + $data = [ + 'name' => 'Updated Role', + ]; + + $response = $this->putJson('/api/v1/roles/' . $role->id, $data); + + $this->assertDatabaseMissing('roles', ['id'=> $role->id, 'name' => 'Updated Role']); + + $response + ->assertStatus(422) + ->assertJsonFragment(['message' => 'The permissions field must be present.']); + } + + /** + * Test the update method of RoleController with wrong permission id. + * + * @group RoleController.update + * @covers ::update + */ + public function test_not_update_roles_with_wrong_permission_id(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_UPDATE_ROLES + ]), + ); + + do { + $randPermId = rand(0, 999); + } while (DB::table('permissions')->where('id', $randPermId)->exists()); + + $data = [ + 'name' => 'Updated Role', + 'permissions' => [ + $randPermId, + ], + ]; + + $response = $this->putJson('/api/v1/roles/' . $role->id, $data); + + $this->assertDatabaseMissing('roles', ['id'=> $role->id, 'name' => 'Updated Role']); + + $response + ->assertStatus(422) + ->assertJsonFragment(['message' => 'The selected permissions.0 is invalid.']); + } + + /** + * Test the update method of RoleController with duplicate name. + * + * @group RoleController.update + * @covers ::update + */ + public function test_not_update_roles_with_duplicate_name(): void + { + $roleOne = Role::factory()->create(); + $roleTwo = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_UPDATE_ROLES + ]), + ); + + $data = [ + 'name' => $roleOne->name, + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->putJson('/api/v1/roles/' . $roleTwo->id, $data); + + $this->assertDatabaseCount('roles', 2); + + $response + ->assertStatus(422) + ->assertJsonFragment(['message' => 'The name has already been taken.']); + } + + /** + * Test the update method of RoleController with unauthorized role permissions. + * + * @group RoleController.update + * @covers ::update + */ + public function test_not_update_roles_with_unauthorized_role_permissions(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_SHOW_ROLES, + RolesPermissions::CAN_CREATE_ROLES, + RolesPermissions::CAN_DELETE_ROLES, + ]), + ); + + $data = [ + 'name' => 'Updated Role', + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->putJson('/api/v1/roles/' . $role->id, $data); + + $this->assertDatabaseMissing('roles', ['id'=> $role->id, 'name' => 'Updated Role']); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the update method of RoleController without any permissions. + * + * @group RoleController.update + * @covers ::update + */ + public function test_not_update_roles_without_any_permission(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create(), + ); + + $data = [ + 'name' => 'Updated Role', + 'permissions' => [ + Permission::findByName(UsersPermissions::CAN_SHOW_USERS, 'web')->id, + ], + ]; + + $response = $this->putJson('/api/v1/roles/' . $role->id, $data); + + $this->assertDatabaseMissing('roles', ['id'=> $role->id, 'name' => 'Updated Role']); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the destroy method of RoleController with minimal permissions. + * + * @group RoleController.destroy + * @covers ::destroy + */ + public function test_destroy_roles_with_minimal_permission(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_DELETE_ROLES + ]), + ); + + $response = $this->deleteJson('/api/v1/roles/' . $role->id); + + $this->assertDatabaseMissing('roles', ['id'=> $role->id]); + + $response + ->assertStatus(200) + ->assertJsonFragment(['data' => 'Role successfully deleted.']); + } + + /** + * Test the destroy method of RoleController with wrong role id. + * + * @group RoleController.destroy + * @covers ::destroy + */ + public function test_not_destroy_roles_with_wrong_role_id(): void + { + Role::factory()->count(3)->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_DELETE_ROLES + ]), + ); + + do { + $randRoleId = rand(0, 999); + } while (DB::table('roles')->where('id', $randRoleId)->exists()); + + $response = $this->deleteJson('/api/v1/roles/' . $randRoleId); + + $this->assertDatabaseCount('roles', 3); + + $response + ->assertStatus(404) + ->assertJsonFragment(['message' => 'No query results for model [App\\Models\\Role] ' . $randRoleId]); + } + + /** + * Test the destroy method of RoleController with unauthorized role permissions. + * + * @group RoleController.destroy + * @covers ::destroy + */ + public function test_not_destroy_roles_with_unauthorized_role_permissions(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create()->givePermissionTo([ + RolesPermissions::CAN_SHOW_ROLES, + RolesPermissions::CAN_CREATE_ROLES, + RolesPermissions::CAN_UPDATE_ROLES, + ]), + ); + + $response = $this->deleteJson('/api/v1/roles/' . $role->id); + + $this->assertDatabaseCount('roles', 1); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } + + /** + * Test the destroy method of RoleController without any permissions. + * + * @group RoleController.destroy + * @covers ::destroy + */ + public function test_not_destroy_roles_without_any_permission(): void + { + $role = Role::factory()->create(); + + Sanctum::actingAs( + User::factory()->create(), + ); + + $response = $this->deleteJson('/api/v1/roles/' . $role->id); + + $this->assertDatabaseCount('roles', 1); + + $response + ->assertStatus(403) + ->assertJsonFragment(['message' => 'User does not have the right permissions.']); + } +}