From dfc2c5180a08fe786756663542b54c916422efc8 Mon Sep 17 00:00:00 2001 From: Lauddar Date: Tue, 30 May 2023 21:16:03 +0200 Subject: [PATCH 1/3] create faqs model and controller --- app/Http/Controllers/api/FaqsController.php | 43 +++++++++++++++++++++ app/Models/Faqs.php | 21 ++++++++++ 2 files changed, 64 insertions(+) create mode 100644 app/Http/Controllers/api/FaqsController.php create mode 100644 app/Models/Faqs.php diff --git a/app/Http/Controllers/api/FaqsController.php b/app/Http/Controllers/api/FaqsController.php new file mode 100644 index 0000000..a366f07 --- /dev/null +++ b/app/Http/Controllers/api/FaqsController.php @@ -0,0 +1,43 @@ + + */ + protected $fillable = [ + 'title', + 'description' + ]; +} From a8be98a7dfff99998c01dd7ff3f35853f99ff91e Mon Sep 17 00:00:00 2001 From: Lauddar Date: Wed, 31 May 2023 10:58:00 +0200 Subject: [PATCH 2/3] implements faqs CRUD features --- app/Http/Controllers/api/FaqController.php | 105 ++++++++++++++++++++ app/Http/Controllers/api/FaqsController.php | 43 -------- app/Models/{Faqs.php => Faq.php} | 2 +- routes/api.php | 9 +- 4 files changed, 113 insertions(+), 46 deletions(-) create mode 100644 app/Http/Controllers/api/FaqController.php delete mode 100644 app/Http/Controllers/api/FaqsController.php rename app/Models/{Faqs.php => Faq.php} (92%) diff --git a/app/Http/Controllers/api/FaqController.php b/app/Http/Controllers/api/FaqController.php new file mode 100644 index 0000000..171f42e --- /dev/null +++ b/app/Http/Controllers/api/FaqController.php @@ -0,0 +1,105 @@ +json(['faqs' => Faq::all()]); + } + + /** + * Get an specific FAQ. + * + * @param Faq $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + $faq = Faq::find($id); + + return response()->json([ + 'id' => $faq->id, + 'title' => $faq->title, + 'description' => $faq->description, + 'created_at'=> $faq->created_at, + 'updated_at' => $faq->updated_at + ]); + } + + /** + * Save a new FAQ from request. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $validatedData = $request->validate([ + 'title' => ['required', 'string', 'max:255'], + 'description' => ['required', 'string'], + ]); + + $faq = Faq::create($validatedData); + + return response()->json(['faq' => $faq], 201); + } + + /** + * Update specific FAQ. + * + * @param Request $request + * @param Faq $id + * @return \Illuminate\Http\JsonResponse + */ + public function update(Request $request, $id) + { + $faqs = Faq::find($id); + + if (!$faqs) { + return response()->json(['error' => 'FAQ not found'], 404); + } + + $validatedData = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'required|string', + ]); + + $faqs->title = $validatedData['title']; + $faqs->description = $validatedData['description']; + $faqs->save(); + + return response()->json(['message' => 'FAQ updated successfully']); + } + + /** + * Delete a specific FAQ. + * + * @param Request $request + * @param Faq $id + * @return \Illuminate\Http\JsonResponse + */ + public function destroy($id) + { + $faqs = Faq::find($id); + + if (!$faqs) { + return response()->json(['error' => 'FAQ not found'], 404); + } + + $faqs->delete(); + + return response()->json(['message' => 'FAQ deleted successfully']); + } +} diff --git a/app/Http/Controllers/api/FaqsController.php b/app/Http/Controllers/api/FaqsController.php deleted file mode 100644 index a366f07..0000000 --- a/app/Http/Controllers/api/FaqsController.php +++ /dev/null @@ -1,43 +0,0 @@ -name('register'); Route::post('/login', [AuthController::class, 'login'])->name('login'); -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { - return $request->user(); +Route::middleware(['auth:api'])->prefix('faqs')->group(function () { + Route::get('/', [FaqController::class, 'index']); + Route::get('/{id}', [FaqController::class, 'show']); + Route::post('/', [FaqController::class, 'store']); + Route::put('/{id}', [FaqController::class, 'update']); + Route::delete('/{id}', [FaqController::class, 'destroy']); }); From 4540bf2ccd97759481374ceb3a010e2a2d076ca2 Mon Sep 17 00:00:00 2001 From: Lauddar Date: Wed, 31 May 2023 15:35:43 +0200 Subject: [PATCH 3/3] add testing and modify controller --- app/Http/Controllers/api/FaqController.php | 55 +-- database/factories/FaqFactory.php | 24 ++ database/factories/UserFactory.php | 2 + database/seeders/DatabaseSeeder.php | 1 + tests/Feature/FaqsTest.php | 378 +++++++++++++++++++++ 5 files changed, 437 insertions(+), 23 deletions(-) create mode 100644 database/factories/FaqFactory.php create mode 100644 tests/Feature/FaqsTest.php diff --git a/app/Http/Controllers/api/FaqController.php b/app/Http/Controllers/api/FaqController.php index 171f42e..de3e823 100644 --- a/app/Http/Controllers/api/FaqController.php +++ b/app/Http/Controllers/api/FaqController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Faq; +use Illuminate\Validation\ValidationException; class FaqController extends Controller { @@ -46,14 +47,18 @@ public function show($id) */ public function store(Request $request) { - $validatedData = $request->validate([ - 'title' => ['required', 'string', 'max:255'], - 'description' => ['required', 'string'], - ]); - - $faq = Faq::create($validatedData); - - return response()->json(['faq' => $faq], 201); + try { + $validatedData = $request->validate([ + 'title' => ['required', 'string', 'max:255'], + 'description' => ['required', 'string'], + ]); + + $faq = Faq::create($validatedData); + + return response()->json(['faq' => $faq], 201); + } catch (ValidationException $e) { + return response()->json(['errors' => $e->errors()], 422); + } } /** @@ -65,22 +70,26 @@ public function store(Request $request) */ public function update(Request $request, $id) { - $faqs = Faq::find($id); - - if (!$faqs) { - return response()->json(['error' => 'FAQ not found'], 404); + try { + $faqs = Faq::find($id); + + if (!$faqs) { + return response()->json(['error' => 'FAQ not found'], 404); + } + + $validatedData = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'required|string', + ]); + + $faqs->title = $validatedData['title']; + $faqs->description = $validatedData['description']; + $faqs->save(); + + return response()->json(['message' => 'FAQ updated successfully']); + } catch (ValidationException $e) { + return response()->json(['errors' => $e->errors()], 422); } - - $validatedData = $request->validate([ - 'title' => 'required|string|max:255', - 'description' => 'required|string', - ]); - - $faqs->title = $validatedData['title']; - $faqs->description = $validatedData['description']; - $faqs->save(); - - return response()->json(['message' => 'FAQ updated successfully']); } /** diff --git a/database/factories/FaqFactory.php b/database/factories/FaqFactory.php new file mode 100644 index 0000000..d1ab4db --- /dev/null +++ b/database/factories/FaqFactory.php @@ -0,0 +1,24 @@ + + */ +class FaqFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'title' => fake()->sentence, + 'description' => fake()->paragraph + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 39de5cb..3de6259 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -23,6 +23,8 @@ public function definition(): array 'email' => fake()->unique()->safeEmail(), 'dni' => $this->faker->regexify('[0-9]{8}[A-Z]'), 'password' => Hash::make('password'), + 'status' => 'ACTIVE', + 'role' => 'ADMIN', 'email_verified_at' => now(), 'remember_token' => Str::random(10), ]; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 7e20284..60f512c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,5 +15,6 @@ public function run(): void // \App\Models\User::factory(10)->create(); \App\Models\User::factory(3)->create(); + \App\Models\Faq::factory(3)->create(); } } diff --git a/tests/Feature/FaqsTest.php b/tests/Feature/FaqsTest.php new file mode 100644 index 0000000..fa0c223 --- /dev/null +++ b/tests/Feature/FaqsTest.php @@ -0,0 +1,378 @@ +create(); + $token = $user->createToken('TestToken')->accessToken; + + $faqs = [ + [ + 'title' => fake()->sentence, + 'description' => fake()->paragraph, + ], + [ + 'title' => fake()->sentence, + 'description' => fake()->paragraph, + ], + ]; + + Faq::insert($faqs); + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->get('/api/faqs'); + + $response->assertStatus(200) + ->assertJson([ + 'faqs' => $faqs + ]); + } + + /** + * Test index method without token. + */ + public function test_index_without_token(): void + { + \Artisan::call('passport:install'); + + $faqs = [ + [ + 'title' => fake()->sentence, + 'description' => fake()->paragraph, + ], + [ + 'title' => fake()->sentence, + 'description' => fake()->paragraph, + ], + ]; + + Faq::insert($faqs); + + $response = $this->get('/api/faqs'); + + $response->assertStatus(302); + } + + /** + * Test show method. + */ + public function test_show() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $faq = Faq::factory()->create(); + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->get('/api/faqs/' . $faq->id); + + $response->assertStatus(200) + ->assertJson([ + 'id' => $faq->id, + 'title' => $faq->title, + 'description' => $faq->description + ]); + } + + /** + * Test show method without token. + */ + public function test_show_without_token() + { + \Artisan::call('passport:install'); + + $faq = Faq::factory()->create(); + + $response = $this->get('/api/faqs/' . $faq->id); + + $response->assertStatus(302); + } + + /** + * Test store method. + */ + public function test_store() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $data = [ + 'title' => 'Test FAQ', + 'description' => 'This is a test FAQ' + ]; + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->post('/api/faqs', $data); + + $response->assertStatus(201) + ->assertJson([ + 'faq' => $data + ]); + + $this->assertDatabaseHas('faqs', $data); + } + + /** + * Test store method without token. + */ + public function test_store_without_token() + { + \Artisan::call('passport:install'); + + $data = [ + 'title' => 'Test FAQ', + 'description' => 'This is a test FAQ' + ]; + + $response = $this->post('/api/faqs', $data); + + $response->assertStatus(302); + } + + /** + * Test store method with missing title and/or description. + */ + public function test_store_with_missing_fields() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + // Missing title + $data1 = [ + 'description' => 'This is a test FAQ' + ]; + + $response1 = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->post('/api/faqs', $data1); + + $response1->assertStatus(422) + ->assertJsonValidationErrors(['title']); + + // Missing description + $data2 = [ + 'title' => 'Test FAQ' + ]; + + $response2 = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->post('/api/faqs', $data2); + + $response2->assertStatus(422) + ->assertJsonValidationErrors(['description']); + + // Missing title and description + $data3 = []; + + $response3 = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->post('/api/faqs', $data3); + + $response3->assertStatus(422) + ->assertJsonValidationErrors(['title', 'description']); + } + + /** + * Test store method with a too long title. + */ + public function test_store_with_long_title() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $data = [ + 'title' => fake()->paragraph . fake()->paragraph . fake()->paragraph, + 'description' => 'This is a test FAQ' + ]; + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->post('/api/faqs', $data); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['title']); + } + + /** + * Test update method. + */ + public function test_update() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $faq = Faq::factory()->create(); + + $data = [ + 'title' => 'Updated FAQ', + 'description' => 'This is an updated FAQ' + ]; + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->put('/api/faqs/' . $faq->id, $data); + + $response->assertStatus(200) + ->assertJson([ + 'message' => 'FAQ updated successfully' + ]); + + $this->assertDatabaseHas('faqs', $data); + } + + /** + * Test update method without token. + */ + public function test_update_without_token() + { + \Artisan::call('passport:install'); + + $faq = Faq::factory()->create(); + + $data = [ + 'title' => 'Updated FAQ', + 'description' => 'This is an updated FAQ' + ]; + + $response = $this->put('/api/faqs/' . $faq->id, $data); + + $response->assertStatus(302); + } + + /** + * Test update method with a non-existent FAQ. + */ + public function test_update_with_non_existent_id() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $data = [ + 'title' => 'Updated FAQ', + 'description' => 'This is an updated FAQ' + ]; + + $nonExistentId = 9999; + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->put('/api/faqs/' . $nonExistentId, $data); + + $response->assertStatus(404); + } + + /** + * Test update method with a too long title. + */ + public function test_update_with_long_title() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $faq = Faq::factory()->create(); + + $data = [ + 'title' => fake()->paragraph . fake()->paragraph . fake()->paragraph, + 'description' => 'This is an updated FAQ' + ]; + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->put('/api/faqs/' . $faq->id, $data); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['title']); + } + + /** + * Test destroy method. + */ + public function test_destroy() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $faqs = Faq::factory(3)->create(); + $faq = $faqs->first(); + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->delete('/api/faqs/' . $faq->id); + + $response->assertStatus(200) + ->assertJson([ + 'message' => 'FAQ deleted successfully' + ]); + + $this->assertDatabaseMissing('faqs', ['id' => $faq->id]); + } + + /** + * Test the destroy method without token. + */ + public function test_destroy_without_token() + { + \Artisan::call('passport:install'); + + $faq = Faq::factory()->create(); + + $response = $this->delete('/api/faqs/' . $faq->id); + + $response->assertStatus(302); + + $this->assertDatabaseHas('faqs', ['id' => $faq->id]); + } + + /** + * Test destroy method with a non-existent FAQ. + */ + public function test_destroy_with_non_existent_id() + { + \Artisan::call('passport:install'); + + $user = User::factory()->create(); + $token = $user->createToken('TestToken')->accessToken; + + $nonExistentId = 9999; + + $response = $this->actingAs($user)->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->delete('/api/faqs/' . $nonExistentId); + + $response->assertStatus(404); + } +} \ No newline at end of file