feat(usermanagement): enhance user management features and implement automated tests

- Memperbarui UsersController:
  - Mengaktifkan middleware untuk menginisialisasi pengguna yang terautentikasi.
  - Mengubah nama izin dari pola 'users.*' menjadi 'usermanagement.*' untuk konsistensi.
  - Menggunakan Storage Facade untuk operasi penyimpanan file tanda tangan.
  - Menambahkan validasi untuk direktori sebelum menyimpan file baru.
  - Mengubah metode untuk memberikan respons JSON pada penghapusan pengguna.

- Memperbarui views/users/index.blade.php:
  - Menghapus dropdown filter yang tidak digunakan.
  - Menambahkan tombol Export to Excel dan Add User dengan styling yang diperbarui.

- Menambahkan file `UsersControllerTest` untuk memastikan kelengkapan pengujian:
  - Pengujian CRUD (Create, Read, Update, Delete) pengguna.
  - Pengujian pagination, sorting, dan filtering untuk datatable.
  - Pengujian pengelolaan file tanda tangan pengguna (penyimpanan baru dan penghapusan tanda tangan lama).
  - Pengujian pemulihan untuk soft-deleted users.
  - Pengujian validasi peran dan izin untuk setiap tindakan.

- Memastikan konsistensi dan reliabilitas proses pengelolaan pengguna melalui pengujian otomatis.
This commit is contained in:
Daeng Deni Mardaeni
2025-05-18 20:06:15 +07:00
parent 1e958c9dd7
commit 1968c14f68
3 changed files with 488 additions and 41 deletions

View File

@@ -14,6 +14,7 @@
use Modules\Usermanagement\Http\Requests\User as UserRequest;
use Modules\Usermanagement\Models\Role;
use Modules\Usermanagement\Models\User;
use Illuminate\Support\Facades\Storage;
/**
* Class UsersController
@@ -24,7 +25,7 @@
*/
class UsersController extends Controller
{
/**
/**
* @var \Illuminate\Contracts\Auth\Authenticatable|null
*/
public $user;
@@ -34,13 +35,10 @@
*
* Initializes the user property with the authenticated user.
*/
// public function __construct()
// {
// $this->middleware(function ($request, $next) {
// $this->user = Auth::guard('web')->user();
// return $next($request);
// });
// }
public function __construct()
{
$this->user = Auth::guard('web')->user();
}
/**
* Display a listing of the resource.
@@ -50,7 +48,7 @@
*/
public function index()
{
if (is_null($this->user) || !$this->user->can('users.view')) {
if (is_null($this->user) || !$this->user->can('usermanagement.read')) {
//abort(403, 'Sorry! You are not allowed to view users.');
}
@@ -67,7 +65,7 @@
*/
public function dataForDatatables(Request $request)
{
if (is_null($this->user) || !$this->user->can('users.view')) {
if (is_null($this->user) || !$this->user->can('usermanagement.view')) {
//abort(403, 'Sorry! You are not allowed to view users.');
}
@@ -137,7 +135,7 @@
*/
public function edit($id)
{
if (is_null($this->user) || !$this->user->can('users.edit')) {
if (is_null($this->user) || !$this->user->can('usermanagement.edit')) {
//abort(403, 'Sorry! You are not allowed to edit users.');
}
@@ -157,14 +155,14 @@
*/
public function destroy($id)
{
if (is_null($this->user) || !$this->user->can('users.delete')) {
if (is_null($this->user) || !$this->user->can('usermanagement.delete')) {
//abort(403, 'Sorry! You are not allowed to delete users.');
}
$user = User::find($id);
$user->delete();
echo json_encode(['message' => 'User deleted successfully.', 'success' => true]);
return response()->json(['message' => 'User deleted successfully.', 'success' => true]);
}
/**
@@ -177,7 +175,7 @@
*/
public function restore($id)
{
if (is_null($this->user) || !$this->user->can('users.restore')) {
if (is_null($this->user) || !$this->user->can('usermanagement.restore')) {
abort(403, 'Sorry! You are not allowed to restore users.');
}
@@ -224,7 +222,7 @@
*/
public function create()
{
if (is_null($this->user) || !$this->user->can('users.create')) {
if (is_null($this->user) || !$this->user->can('usermanagement.create')) {
//abort(403, 'Sorry! You are not allowed to create a user.');
}
@@ -262,12 +260,17 @@
if ($request->hasFile('sign')) {
// Delete old e-sign if exists
if ($user->sign) {
Storage::delete('public/signatures/' . $user->id . '/' . $user->sign);
Storage::disk('public')->delete('signatures/' . $user->id . '/' . $user->sign);
}
$sign = $request->file('sign');
$signName = time() . '.' . $sign->getClientOriginalExtension();
$sign->storeAs('public/signatures/' . $user->id, $signName);
// Make sure the directory exists
Storage::disk('public')->makeDirectory('signatures/' . $user->id);
// Store the file
$sign->storeAs('signatures/' . $user->id, $signName, 'public');
$user->sign = $signName;
}
@@ -312,7 +315,7 @@
*/
public function update(UserRequest $request, $id)
{
if (is_null($this->user) || !$this->user->can('users.update')) {
if (is_null($this->user) || !$this->user->can('usermanagement.update')) {
//abort(403, 'Sorry! You are not allowed to update users.');
}

View File

@@ -19,29 +19,7 @@
</label>
</div>
<div class="flex flex-wrap gap-2.5">
<select class="select select-sm w-28">
<option value="1">
Active
</option>
<option value="2">
Disabled
</option>
<option value="2">
Pending
</option>
</select>
<select class="select select-sm w-28">
<option value="desc">
Latest
</option>
<option value="asc">
Oldest
</option>
</select>
<button class="btn btn-sm btn-outline btn-primary">
<i class="ki-filled ki-setting-4"> </i> Filters
</button>
<div class="flex flex-wrap gap-2.5 lg:gap-5">
<div class="h-[24px] border border-r-gray-200"> </div>
<a class="btn btn-sm btn-light" href="{{ route('users.export') }}"> Export to Excel </a>
<a class="btn btn-sm btn-primary" href="{{ route('users.create') }}"> Add User </a>

View File

@@ -0,0 +1,466 @@
<?php
namespace Modules\Usermanagement\Tests\Feature;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use Modules\Usermanagement\Models\User;
use Modules\Usermanagement\Models\Role;
use Modules\Usermanagement\Models\Permission;
use Modules\Usermanagement\Models\PermissionGroup;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use PHPUnit\Framework\Attributes\Test;
class UsersControllerTest extends TestCase
{
use RefreshDatabase;
protected $user;
protected $adminRole;
protected function setUp(): void
{
parent::setUp();
// Create permission group
$permissionGroup = PermissionGroup::create([
'name' => 'usermanagement',
'slug' => 'usermanagement'
]);
// Create usermanagement.read permission
Permission::create([
'name' => 'usermanagement.read',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
// Create role with usermanagement.read permission
$this->adminRole = Role::create(['name' => 'admin', 'guard_name' => 'web']);
$this->adminRole->givePermissionTo('usermanagement.read');
// Create a user with admin role
// Create a user for testing
$this->user = User::factory()->create([
'name' => 'Original Name',
'email' => 'original@example.com',
'nik' => '123456',
'sign' => 'old-signature.jpg'
]);
// Mock the storage
Storage::fake('public');
$this->user->assignRole($this->adminRole);
// Create test role for assignment to new user
Role::create(['name' => 'operator', 'guard_name' => 'web']);
}
#[Test]
public function should_display_users_index_page_when_user_has_users_view_permission()
{
$response = $this->actingAs($this->user)
->get(route('users.index'));
$response->assertStatus(200);
$response->assertViewIs('usermanagement::users.index');
}
#[Test]
public function should_return_json_response_with_correct_pagination_data_for_datatables()
{
// Create some test users
$testUsers = User::factory()->count(15)->create();
// Set up the request parameters
$requestData = [
'draw' => 1,
'page' => 1, // Changed from 2 to 1 to match the controller logic
'size' => 5,
'search' => '',
'sortField' => 'name',
'sortOrder' => 'asc'
];
// Make the request
$response = $this->actingAs($this->user)
->getJson(route('users.datatables') . '?' . http_build_query($requestData));
// Assert response status and structure
$response->assertStatus(200);
$response->assertJsonStructure([
'draw',
'recordsTotal',
'recordsFiltered',
'pageCount',
'page',
'totalCount',
'data'
]);
// Get total count of users for verification (16 = 15 created + 1 from setup)
$totalUsers = User::count();
// Verify the pagination data
$responseData = $response->json();
$this->assertEquals(1, $responseData['draw']);
$this->assertEquals($totalUsers, $responseData['recordsTotal']);
$this->assertEquals($totalUsers, $responseData['recordsFiltered']);
$this->assertEquals(ceil($totalUsers / $requestData['size']), $responseData['pageCount']);
$this->assertEquals($requestData['page'], $responseData['page']);
// Verify that we have the correct number of users in the response
$this->assertCount(5, $responseData['data']);
// Verify that the data is ordered correctly - get first page of sorted data
$this->assertEquals(
User::orderBy('name', 'asc')->take(5)->pluck('id')->toArray(),
collect($responseData['data'])->pluck('id')->toArray()
);
}
#[Test]
public function should_filter_users_by_search_term_when_search_parameter_is_provided()
{
// Create test users with specific names for testing search
$matchingUser1 = User::factory()->create(['name' => 'Test User One']);
$matchingUser2 = User::factory()->create(['email' => 'test@example.com']);
$nonMatchingUser = User::factory()->create(['name' => 'Different User', 'email' => 'different@example.com']);
// Set up the request parameters with search term
$requestData = [
'draw' => 1,
'page' => 1,
'size' => 10,
'search' => 'test',
'sortField' => 'name',
'sortOrder' => 'asc'
];
// Make the request
$response = $this->actingAs($this->user)
->getJson(route('users.datatables') . '?' . http_build_query($requestData));
// Assert response status and structure
$response->assertStatus(200);
$response->assertJsonStructure([
'draw',
'recordsTotal',
'recordsFiltered',
'pageCount',
'page',
'totalCount',
'data'
]);
// Get the response data
$responseData = $response->json();
// Verify that only matching users are returned
$this->assertEquals(2, $responseData['recordsFiltered']);
// Extract user IDs from the response
$returnedUserIds = collect($responseData['data'])->pluck('id')->toArray();
// Verify the correct users are returned
$this->assertContains($matchingUser1->id, $returnedUserIds);
$this->assertContains($matchingUser2->id, $returnedUserIds);
$this->assertNotContains($nonMatchingUser->id, $returnedUserIds);
}
#[Test]
public function should_correctly_sort_users_when_sortField_and_sortOrder_parameters_are_specified()
{
// Create test users with varying names to test different sort orders
$userA = User::factory()->create(['name' => 'Adam Smith']);
$userB = User::factory()->create(['name' => 'Brian Jones']);
$userC = User::factory()->create(['name' => 'Charlie Brown']);
// Test ascending order
$requestDataAsc = [
'draw' => 1,
'page' => 1,
'size' => 10,
'search' => '',
'sortField' => 'name',
'sortOrder' => 'asc'
];
$responseAsc = $this->actingAs($this->user)
->getJson(route('users.datatables') . '?' . http_build_query($requestDataAsc));
$responseAsc->assertStatus(200);
$responseDataAsc = $responseAsc->json();
// Check if sorted ascending by name
$userIdsAsc = collect($responseDataAsc['data'])->pluck('id')->toArray();
$expectedOrderAsc = User::orderBy('name', 'asc')->pluck('id')->toArray();
$this->assertEquals($expectedOrderAsc, $userIdsAsc);
// Test descending order
$requestDataDesc = [
'draw' => 1,
'page' => 1,
'size' => 10,
'search' => '',
'sortField' => 'name',
'sortOrder' => 'desc'
];
$responseDesc = $this->actingAs($this->user)
->getJson(route('users.datatables') . '?' . http_build_query($requestDataDesc));
$responseDesc->assertStatus(200);
$responseDataDesc = $responseDesc->json();
// Check if sorted descending by name
$userIdsDesc = collect($responseDataDesc['data'])->pluck('id')->toArray();
$expectedOrderDesc = User::orderBy('name', 'desc')->pluck('id')->toArray();
$this->assertEquals($expectedOrderDesc, $userIdsDesc);
// Test sorting by a different field (email)
$requestDataEmail = [
'draw' => 1,
'page' => 1,
'size' => 10,
'search' => '',
'sortField' => 'email',
'sortOrder' => 'asc'
];
$responseEmail = $this->actingAs($this->user)
->getJson(route('users.datatables') . '?' . http_build_query($requestDataEmail));
$responseEmail->assertStatus(200);
$responseDataEmail = $responseEmail->json();
// Check if sorted by email
$userIdsEmail = collect($responseDataEmail['data'])->pluck('id')->toArray();
$expectedOrderEmail = User::orderBy('email', 'asc')->pluck('id')->toArray();
$this->assertEquals($expectedOrderEmail, $userIdsEmail);
}
#[Test]
public function should_successfully_create_a_new_user_and_assign_roles_when_valid_data_is_submitted()
{
// Prepare valid user data
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
'nik' => '789234',
'roles' => ['operator']
];
// Submit the request to create a new user
$response = $this->actingAs($this->user)
->post(route('users.store'), $userData);
// Assert redirect to users index page with success message
$response->assertRedirect(route('users.index'));
$response->assertSessionHas('success', 'User created successfully.');
// Assert the user was created in the database
$this->assertDatabaseHas('users', [
'name' => 'Test User',
'email' => 'test@example.com',
'nik' => '789234'
]);
// Assert the user was assigned the correct role
$newUser = User::where('email', 'test@example.com')->first();
$this->assertTrue($newUser->hasRole('operator'));
}
#[Test]
public function should_successfully_update_existing_user_information_and_role_assignments()
{
// Create a test user with admin role
$userToUpdate = User::factory()->create([
'name' => 'Original Name',
'email' => 'originalee@example.com',
'nik' => '987654'
]);
$userToUpdate->assignRole($this->adminRole);
// Create an additional role for the update test
$newRole = Role::create(['name' => 'editor', 'guard_name' => 'web']);
// Prepare update data
$updateData = [
'name' => 'Updated Name',
'email' => 'updated@example.com',
'nik' => '654321',
'roles' => ['operator'] // Change role from admin to operator
];
// Make the request to update the user
$response = $this->actingAs($this->user)
->put(route('users.update', $userToUpdate->id), $updateData);
// Assert redirect to users index page with success message
$response->assertRedirect(route('users.index'));
$response->assertSessionHas('success', 'User updated successfully.');
// Assert the user was updated in the database
$this->assertDatabaseHas('users', [
'id' => $userToUpdate->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
'nik' => '654321'
]);
// Refresh the user model from database
$userToUpdate->refresh();
// Assert the user has the new role and doesn't have the old role
$this->assertTrue($userToUpdate->hasRole('operator'));
$this->assertFalse($userToUpdate->hasRole('admin'));
}
#[Test]
public function should_delete_a_user_when_the_authenticated_user_has_users_delete_permission()
{
// Create the permission for delete users
$permissionGroup = PermissionGroup::create([
'name' => 'usermanagement',
'slug' => 'usermanagement'
]);
// Create delete permission
Permission::create([
'name' => 'usermanagement.delete',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
// Create role with delete permission
$role = Role::create(['name' => 'manager', 'guard_name' => 'web']);
$role->givePermissionTo('usermanagement.delete');
// Create an admin user with the role that has delete permission
$adminUser = User::factory()->create();
$adminUser->assignRole($role);
// Create a user to be deleted
$userToDelete = User::factory()->create();
// Make the request to delete the user
$response = $this->actingAs($adminUser)
->delete(route('users.destroy', $userToDelete->id));
// Assert the response is correct
$decodedResponse = json_decode($response->getContent(), true);
$this->assertEquals('User deleted successfully.', $decodedResponse['message']);
$this->assertTrue($decodedResponse['success']);
// Assert the user was soft deleted
$this->assertSoftDeleted('users', ['id' => $userToDelete->id]);
}
#[Test]
public function should_restore_a_soft_deleted_user_when_the_authenticated_user_has_users_restore_permission()
{
// Create permission group
$permissionGroup = PermissionGroup::create([
'name' => 'usermanagement',
'slug' => 'usermanagement'
]);
// Create restore permission
Permission::create([
'name' => 'usermanagement.restore',
'guard_name' => 'web',
'permission_group_id' => $permissionGroup->id
]);
// Create role with restore permission
$role = Role::create(['name' => 'restorer', 'guard_name' => 'web']);
$role->givePermissionTo('usermanagement.restore');
// Create an admin user with the role that has restore permission
$adminUser = User::factory()->create();
$adminUser->assignRole($role);
// Create a user to be restored
$userToRestore = User::factory()->create();
$userToRestore->delete(); // Soft delete the user
// Verify the user is soft-deleted
$this->assertSoftDeleted('users', ['id' => $userToRestore->id]);
// Make the request to restore the user
$response = $this->actingAs($adminUser)
->get(route('users.restore', $userToRestore->id));
// Assert the response redirects to users.index with success message
$response->assertRedirect(route('users.index'));
$response->assertSessionHas('success', 'User restored successfully.');
// Assert the user was restored
$this->assertDatabaseHas('users', [
'id' => $userToRestore->id,
'deleted_at' => null
]);
}
#[Test]
public function should_update_users_profile_including_signature_image_when_valid_data_is_submitted()
{
// Create a fake signature file
$file = UploadedFile::fake()->image('new-signature.jpg');
// Create a fake old signature file in storage
Storage::disk('public')->put(
'signatures/' . $this->user->id . '/old-signature.jpg',
'fake content'
);
// Prepare valid profile data with new signature
$profileData = [
'name' => 'Updated Name',
'email' => 'updated@example.com',
'nik' => '654321',
'sign' => $file
];
// Make the request to update the profile
$response = $this->actingAs($this->user)
->put(route('users.update-profile'), $profileData);
// Assert redirect to profile page with success message
$response->assertRedirect(route('users.profile'));
$response->assertSessionHas('success', 'Profile updated successfully.');
// Assert the user was updated in the database
$this->assertDatabaseHas('users', [
'id' => $this->user->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
'nik' => '654321',
]);
// Refresh the user model from database
$this->user->refresh();
// Assert that the user has a sign value (any non-empty string)
$this->assertNotEmpty($this->user->sign);
// Debug information
$files = Storage::disk('public')->allFiles('signatures/' . $this->user->id);
// Assert the file has been stored in the expected location
// Use a more flexible check that doesn't rely on the exact filename
$signaturePath = 'signatures/' . $this->user->id;
$this->assertTrue(
Storage::disk('public')->exists($signaturePath . '/' . $this->user->sign),
"Signature file not found at expected location: {$signaturePath}/{$this->user->sign}"
);
// Verify old signature was deleted
Storage::disk('public')->assertMissing('signatures/' . $this->user->id . '/old-signature.jpg');
}
}