Skip to content

Commit b3eaae8

Browse files
authored
Resolves laravel-portugal#30 - An authenticated user can delete a question (laravel-portugal#50)
* Update CHANGELOG.md * added delete question * Change parameter order to align with project style
1 parent e8753b5 commit b3eaae8

9 files changed

+125
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to `laravel-portugal/api` will be documented in this file
66

77
### Added
88

9+
- An authenticated user can delete a question (#30)
910
- A guest or an authenticated user can see details of a question (#48)
1011

1112
### Changed

domains/Discussions/Controllers/AnswersStoreController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function __construct(Answer $answer, Question $question)
1919
$this->question = $question;
2020
}
2121

22-
public function __invoke(int $questionId, Request $request): Response
22+
public function __invoke(Request $request, int $questionId): Response
2323
{
2424
$this->validate($request, [
2525
'content' => ['required', 'string'],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use Domains\Discussions\Models\Question;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Http\Response;
9+
10+
class QuestionsDeleteController extends Controller
11+
{
12+
private Question $questions;
13+
14+
public function __construct(Question $questions)
15+
{
16+
$this->questions = $questions;
17+
}
18+
19+
public function __invoke(Request $request, int $questionId): Response
20+
{
21+
$question = $this->questions->findOrFail($questionId);
22+
23+
$this->authorize('delete', $question);
24+
25+
$question->delete();
26+
27+
return new Response('', Response::HTTP_NO_CONTENT);
28+
}
29+
}

domains/Discussions/Controllers/QuestionsUpdateController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function __construct(Question $questions)
1616
$this->questions = $questions;
1717
}
1818

19-
public function __invoke(int $questionId, Request $request): Response
19+
public function __invoke(Request $request, int $questionId): Response
2020
{
2121
$question = $this->questions->findOrFail($questionId);
2222

domains/Discussions/Database/Migrations/2020_10_12_104113_create_questions_table.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public function up(): void
1111
{
1212
Schema::create('questions', function (Blueprint $table) {
1313
$table->id();
14-
$table->foreignIdFor(User::class, 'author_id');
14+
$table->foreignIdFor(User::class, 'author_id')->constrained();
1515
$table->string('title')->index();
1616
$table->string('slug');
1717
$table->text('description');

domains/Discussions/Policies/QuestionPolicy.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Domains\Discussions\Policies;
44

5+
use Domains\Accounts\Enums\AccountTypeEnum;
56
use Domains\Accounts\Models\User;
67
use Domains\Discussions\Models\Question;
78
use Illuminate\Auth\Access\HandlesAuthorization;
@@ -10,8 +11,13 @@ class QuestionPolicy
1011
{
1112
use HandlesAuthorization;
1213

13-
public function update(User $user, Question $question)
14+
public function update(User $user, Question $question): bool
1415
{
1516
return $question->author->is($user);
1617
}
18+
19+
public function delete(User $user, Question $question): bool
20+
{
21+
return $question->author->is($user) || $user->hasRole(AccountTypeEnum::ADMIN);
22+
}
1723
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Tests\Feature;
4+
5+
use Domains\Accounts\Database\Factories\UserFactory;
6+
use Domains\Accounts\Enums\AccountTypeEnum;
7+
use Domains\Accounts\Models\User;
8+
use Domains\Discussions\Database\Factories\QuestionFactory;
9+
use Domains\Discussions\Models\Question;
10+
use Faker\Factory;
11+
use Faker\Generator;
12+
use Illuminate\Http\Response;
13+
use Illuminate\Support\Carbon;
14+
use Laravel\Lumen\Testing\DatabaseMigrations;
15+
use Tests\TestCase;
16+
17+
class QuestionsDeleteTest extends TestCase
18+
{
19+
use DatabaseMigrations;
20+
21+
private Generator $faker;
22+
private User $user;
23+
private Question $question;
24+
25+
protected function setUp(): void
26+
{
27+
parent::setUp();
28+
29+
$this->faker = Factory::create();
30+
$this->user = UserFactory::new()->create();
31+
$this->question = QuestionFactory::new(['author_id' => $this->user->id])->create();
32+
33+
Carbon::setTestNow();
34+
}
35+
36+
/** @test */
37+
public function it_soft_deletes_a_question_i_own(): void
38+
{
39+
$response = $this->actingAs($this->user)
40+
->delete(route('discussions.questions.delete', ['questionId' => $this->question->id]));
41+
42+
$this->assertResponseStatus(Response::HTTP_NO_CONTENT);
43+
self::assertTrue($response->response->isEmpty());
44+
$this->seeInDatabase('questions', [
45+
'id' => $this->question->id,
46+
'updated_at' => Carbon::now(),
47+
'deleted_at' => Carbon::now(),
48+
]);
49+
}
50+
51+
/** @test */
52+
public function it_allows_admin_to_soft_delete_another_users_question(): void
53+
{
54+
$response = $this->actingAs(UserFactory::new()->withRole(AccountTypeEnum::ADMIN)->make())
55+
->delete(route('discussions.questions.update', ['questionId' => $this->question->id]));
56+
57+
$this->assertResponseStatus(Response::HTTP_NO_CONTENT);
58+
self::assertTrue($response->response->isEmpty());
59+
$this->seeInDatabase('questions', [
60+
'id' => $this->question->id,
61+
'updated_at' => Carbon::now(),
62+
'deleted_at' => Carbon::now(),
63+
]);
64+
}
65+
66+
/** @test */
67+
public function it_forbids_a_non_admin_to_soft_delete_a_question_he_doesnt_own(): void
68+
{
69+
$this->actingAs(UserFactory::new()->make())
70+
->delete(route('discussions.questions.update', ['questionId' => $this->question->id]));
71+
72+
$this->assertResponseStatus(Response::HTTP_FORBIDDEN);
73+
$this->seeInDatabase('questions', [
74+
'id' => $this->question->id,
75+
'updated_at' => $this->question->updated_at,
76+
'deleted_at' => null,
77+
]);
78+
}
79+
}

domains/Discussions/Tests/Feature/QuestionsUpdateTest.php

-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ public function it_updates_questions(): void
4949
);
5050

5151
$this->assertResponseStatus(Response::HTTP_NO_CONTENT);
52-
5352
self::assertTrue($response->isEmpty());
54-
5553
$this->seeInDatabase('questions', [
5654
'id' => $this->question->id,
5755
'author_id' => $this->user->id,

domains/Discussions/routes.php

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Domains\Discussions\Controllers\AnswersStoreController;
4+
use Domains\Discussions\Controllers\QuestionsDeleteController;
45
use Domains\Discussions\Controllers\QuestionsStoreController;
56
use Domains\Discussions\Controllers\QuestionsUpdateController;
67
use Domains\Discussions\Controllers\QuestionsViewController;
@@ -17,6 +18,11 @@
1718
'uses' => QuestionsUpdateController::class,
1819
]);
1920

21+
Route::delete('/questions/{questionId}', [
22+
'as' => 'questions.delete',
23+
'uses' => QuestionsDeleteController::class
24+
]);
25+
2026
Route::post('/questions/{questionId}/answers', [
2127
'as' => 'questions.answers',
2228
'uses' => AnswersStoreController::class

0 commit comments

Comments
 (0)