diff --git a/app/Http/Controllers/RequestController.php b/app/Http/Controllers/RequestController.php new file mode 100644 index 00000000..78db9f9e --- /dev/null +++ b/app/Http/Controllers/RequestController.php @@ -0,0 +1,98 @@ +middleware('permission:' . RequestPermissions::CAN_VIEW_REQUESTS)->only(['index', 'show']); + $this->middleware('permission:' . RequestPermissions::CAN_DELETE_REQUESTS)->only(['destroy']); + } + + /** + * Retrieve a paginated list of requests. + * + * @param Request $httpRequest + * @return \Illuminate\Pagination\LengthAwarePaginator + */ + public function index(Request $httpRequest): \Illuminate\Pagination\LengthAwarePaginator + { + $httpRequest->validate([ + 'sort' => 'string|in:id,id:asc,id:desc,name,name:asc,name:desc,created_at,created_at:asc,created_at:desc,', + 'per_page' => 'integer|between:1,50', + ]); + + if ($httpRequest->sort !== null) { + $sort = explode(':', $httpRequest->sort); + $requests = WishRequest::orderBy($sort[0], $sort[1] ?? 'asc')->paginate($httpRequest->per_page ?? 25); + } else { + $requests = WishRequest::orderBy('created_at')->paginate($httpRequest->per_page ?? 25); + } + + return $requests; + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $httpRequest) + { + $httpRequest->validate([ + 'name' => 'required|string|max:255', + 'message' => 'required|string', + ]); + + $request = WishRequest::create([ + 'name' => $httpRequest->name, + 'message' => $httpRequest->message, + ]); + + if ($request === null) { + // @codeCoverageIgnoreStart + return new ApiErrorResponse('Failed to create request'); + // @codeCoverageIgnoreEnd + } else { + return new ApiSuccessResponse($request, status: Response::HTTP_CREATED); + } + } + + /** + * Display the specified resource. + */ + public function show(WishRequest $request) + { + return new ApiSuccessResponse($request); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(WishRequest $request) + { + if($request->delete()) { + return new ApiSuccessResponse('', status: Response::HTTP_NO_CONTENT); + } else { + // @codeCoverageIgnoreStart + return new ApiErrorResponse('Failed to delete'); + // @codeCoverageIgnoreEnd + } + } +} diff --git a/app/Models/Request.php b/app/Models/Request.php new file mode 100644 index 00000000..0f68ab1b --- /dev/null +++ b/app/Models/Request.php @@ -0,0 +1,40 @@ + + */ + protected $fillable = [ + 'name', + 'message', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'updated_at', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; +} diff --git a/app/Permissions/RequestPermissions.php b/app/Permissions/RequestPermissions.php new file mode 100644 index 00000000..58a49058 --- /dev/null +++ b/app/Permissions/RequestPermissions.php @@ -0,0 +1,17 @@ + + */ +class RequestFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->name, + 'message' => $this->faker->paragraph() + ]; + } +} diff --git a/database/migrations/2023_12_29_205859_create_requests_table.php b/database/migrations/2023_12_29_205859_create_requests_table.php new file mode 100644 index 00000000..4f35018b --- /dev/null +++ b/database/migrations/2023_12_29_205859_create_requests_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->text('message')->max(2000); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('requests'); + } +}; diff --git a/database/migrations/2023_12_29_212656_add_request_permissions.php b/database/migrations/2023_12_29_212656_add_request_permissions.php new file mode 100644 index 00000000..857141d8 --- /dev/null +++ b/database/migrations/2023_12_29_212656_add_request_permissions.php @@ -0,0 +1,34 @@ + 'requests.view', + 'guard_name' => 'web', + ]); + Permission::create([ + 'name' => 'requests.delete', + 'guard_name' => 'web', + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('permissions')->whereIn('name', [ + 'requests.view', + 'requests.delete', + ])->delete(); + } +}; diff --git a/routes/api.php b/routes/api.php index 2ea2bda2..b1083417 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,5 +24,6 @@ require __DIR__ . '/api/v1/auth.php'; require __DIR__ . '/api/v1/user.php'; require __DIR__ . '/api/v1/role.php'; + require __DIR__ . '/api/v1/request.php'; require __DIR__ . '/api/v1/permission.php'; }); diff --git a/routes/api/v1/request.php b/routes/api/v1/request.php new file mode 100644 index 00000000..07d1ef59 --- /dev/null +++ b/routes/api/v1/request.php @@ -0,0 +1,13 @@ +name('api.v1.requests.store'); + +Route::group(['middleware' => 'auth:sanctum'], function () { + Route::get('/requests', [RequestController::class, 'index'])->name('api.v1.requests.index'); + Route::get('/requests/{request}', [RequestController::class, 'show'])->name('api.v1.requests.show'); + Route::delete('/requests/{request}', [RequestController::class, 'destroy'])->name('api.v1.requests.destroy'); +}); diff --git a/tests/Feature/Http/Controllers/RequestControllerTest.php b/tests/Feature/Http/Controllers/RequestControllerTest.php new file mode 100644 index 00000000..96a06449 --- /dev/null +++ b/tests/Feature/Http/Controllers/RequestControllerTest.php @@ -0,0 +1,200 @@ +count(10)->create(); + + Sanctum::actingAs(User::factory()->create()->givePermissionTo([ + RequestPermissions::CAN_VIEW_REQUESTS, + ]) + ); + + $response = $this->getJson('/api/v1/requests'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'name', + 'message', + 'created_at', + ], + ], + 'links' => [ + '*' => [ + 'url', + 'label', + 'active', + ], + ], + 'first_page_url', + 'from', + 'last_page', + 'last_page_url', + 'next_page_url', + 'path', + 'per_page', + 'prev_page_url', + 'to', + 'total', + ]); + + $response->assertJsonFragment([ + 'id' => $testReq[0]->id, + 'name' => $testReq[0]->name, + 'message' => $testReq[0]->message, + 'created_at' => $testReq[0]->created_at, + ]); + } + + /** + * Test retrieving a paginated list of requests with sorting. + * + * @covers ::index + */ + public function test_list_requests_with_sorting(): void + { + $testReq = Request::factory()->count(10)->create(); + + Sanctum::actingAs(User::factory()->create()->givePermissionTo([ + RequestPermissions::CAN_VIEW_REQUESTS, + ]) + ); + + $response = $this->getJson('/api/v1/requests?sort=id:desc&per_page=5'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'name', + 'message', + 'created_at', + ], + ], + 'links' => [ + '*' => [ + 'url', + 'label', + 'active', + ], + ], + 'first_page_url', + 'from', + 'last_page', + 'last_page_url', + 'next_page_url', + 'path', + 'per_page', + 'prev_page_url', + 'to', + 'total', + ]); + + $testReq = $testReq->sortByDesc('id')->values()->take(5); + + $response->assertJsonFragment([ + 'id' => $testReq[0]->id, + 'name' => $testReq[0]->name, + 'message' => $testReq[0]->message, + 'created_at' => $testReq[0]->created_at, + ]); + } + + /** + * Test storing a newly created request. + * + * @covers ::store + */ + public function test_store_request(): void + { + $data = [ + 'name' => 'Test Request', + 'message' => 'This is a test request.', + ]; + + $response = $this->postJson('/api/v1/requests', $data); + + $this->assertDatabaseHas('requests', [ + 'name' => $data['name'], + 'message' => $data['message'], + ]); + + $response->assertStatus(Response::HTTP_CREATED); + + $response->assertJsonFragment([ + 'name' => $data['name'], + 'message' => $data['message'], + ]); + } + + /** + * Test displaying a specific request. + * + * @covers ::show + */ + public function test_show_single_request(): void + { + $request = Request::factory(5)->create(); + + Sanctum::actingAs(User::factory()->create()->givePermissionTo([ + RequestPermissions::CAN_VIEW_REQUESTS, + ]) + ); + + $response = $this->getJson('/api/v1/requests/'.$request->get(1)->id); + + $response->assertStatus(Response::HTTP_OK) + ->assertJsonFragment([ + 'name' => $request->get(1)->name, + 'message' => $request->get(1)->message, + 'created_at' => $request->get(1)->created_at, + ]); + } + + /** + * Test deleting a specific request. + * + * @covers ::destroy + */ + public function test_destroy(): void + { + $request = Request::factory(3)->create(); + + Sanctum::actingAs(User::factory()->create()->givePermissionTo([ + RequestPermissions::CAN_DELETE_REQUESTS, + ]) + ); + + $response = $this->delete('/api/v1/requests/' . $request->get(1)->id); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + $this->assertDatabaseMissing('requests', ['id' => $request->get(1)->id]); + } +}