Skip to content
This repository was archived by the owner on Jun 29, 2021. It is now read-only.

Commit 0b3182e

Browse files
authored
Resolve #31 - An authenticated user can post an answer to a question (#46)
* Resolve #31 - An authenticated user can post an answer to a question * Resolve #31 - An authenticated user can post an answer to a question * Resolve #31 - An authenticated user can post an answer to a question * Resolve #31 - An authenticated user can post an answer to a question * Resolve #31 - An authenticated user can post an answer to a question (Rename Table name) * Resolve #31 - An authenticated user can post an answer to a question (Request Changed) * Resolve #31 - An authenticated user can post an answer to a question (Request Changed) * Resolve #31 - An authenticated user can post an answer to a question (Resolve conflicts)
1 parent 272be59 commit 0b3182e

File tree

8 files changed

+277
-0
lines changed

8 files changed

+277
-0
lines changed

CHANGELOG.md

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

99
- First version of the API documentation
1010
- A guest should be able to login and logout (#37)
11+
- An authenticated user can post an answer to a question (#31)
1112

1213
### Changed
1314

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use Domains\Discussions\Models\Answer;
7+
use Domains\Discussions\Models\Question;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Http\Response;
10+
11+
class AnswersStoreController extends Controller
12+
{
13+
private Answer $answer;
14+
private Question $question;
15+
16+
public function __construct(Answer $answer, Question $question)
17+
{
18+
$this->answer = $answer;
19+
$this->question = $question;
20+
}
21+
22+
public function __invoke(int $questionId, Request $request): Response
23+
{
24+
$this->validate($request, [
25+
'content' => ['required', 'string'],
26+
]);
27+
28+
$question = $this->question->findOrFail($questionId);
29+
30+
$this->answer
31+
->forceFill([
32+
'author_id' => $request->user()->id,
33+
'question_id' => $question->id,
34+
'content' => $request->input('content'),
35+
])
36+
->save();
37+
38+
return new Response('', Response::HTTP_NO_CONTENT);
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Database\Factories;
4+
5+
use Carbon\Carbon;
6+
use Domains\Accounts\Database\Factories\UserFactory;
7+
use Domains\Discussions\Models\Answer;
8+
use Illuminate\Database\Eloquent\Factories\Factory;
9+
10+
class AnswerFactory extends Factory
11+
{
12+
protected $model = Answer::class;
13+
14+
public function definition(): array
15+
{
16+
return [
17+
'author_id' => UserFactory::new(),
18+
'question_id' => QuestionFactory::new(),
19+
'content' => $this->faker->paragraph,
20+
'created_at' => Carbon::now(),
21+
'updated_at' => Carbon::now(),
22+
];
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
use Domains\Accounts\Models\User;
4+
use Domains\Discussions\Models\Question;
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
class CreateQuestionAnswersTable extends Migration
10+
{
11+
public function up(): void
12+
{
13+
Schema::create('question_answers', function (Blueprint $table) {
14+
$table->id();
15+
$table->foreignIdFor(User::class, 'author_id');
16+
$table->foreignIdFor(Question::class, 'question_id');
17+
$table->text('content');
18+
$table->timestampsTz();
19+
$table->softDeletesTz();
20+
21+
$table->index('question_id');
22+
$table->index('author_id');
23+
$table->index('created_at');
24+
});
25+
}
26+
}

domains/Discussions/Models/Answer.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Models;
4+
5+
use Domains\Accounts\Models\User;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
use Illuminate\Database\Eloquent\SoftDeletes;
9+
10+
class Answer extends Model
11+
{
12+
use SoftDeletes;
13+
14+
protected $table = 'question_answers';
15+
16+
public function author(): BelongsTo
17+
{
18+
return $this->belongsTo(User::class)
19+
->withTrashed();
20+
}
21+
22+
public function question(): BelongsTo
23+
{
24+
return $this->belongsTo(Question::class)
25+
->withTrashed();
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Tests\Feature;
4+
5+
use Domains\Accounts\Database\Factories\UserFactory;
6+
use Domains\Accounts\Models\User;
7+
use Domains\Discussions\Database\Factories\QuestionFactory;
8+
use Domains\Discussions\Models\Question;
9+
use Faker\Factory;
10+
use Faker\Generator;
11+
use Illuminate\Http\Response;
12+
use Laravel\Lumen\Testing\DatabaseMigrations;
13+
use Tests\TestCase;
14+
15+
class AnswersStoreTest extends TestCase
16+
{
17+
use DatabaseMigrations;
18+
19+
private Generator $faker;
20+
private User $user;
21+
private Question $question;
22+
23+
protected function setUp(): void
24+
{
25+
parent::setUp();
26+
27+
$this->faker = Factory::create();
28+
$this->user = UserFactory::new()->create();
29+
$this->question = QuestionFactory::new(['author_id' => $this->user->id])->create();
30+
}
31+
32+
/** @test */
33+
public function it_stores_answer(): void
34+
{
35+
$payload = [
36+
'content' => $this->faker->paragraph,
37+
];
38+
39+
$response = $this->actingAs($this->user)
40+
->call('POST', route('discussions.questions.answers', ['questionId' => $this->question->id]), $payload)
41+
->assertStatus(Response::HTTP_NO_CONTENT);
42+
43+
self::assertTrue($response->isEmpty());
44+
45+
$this->seeInDatabase('question_answers', [
46+
'author_id' => $this->user->id,
47+
'question_id' => $this->question->id,
48+
'content' => $payload['content']
49+
]);
50+
}
51+
52+
/** @test */
53+
public function it_forbids_guests_to_store_answer(): void
54+
{
55+
$this->post(route('discussions.questions.answers', ['questionId' => $this->question->id]))
56+
->assertResponseStatus(Response::HTTP_UNAUTHORIZED);
57+
}
58+
59+
/** @test */
60+
public function it_fails_to_store_answer_on_validation_errors(): void
61+
{
62+
$this->actingAs($this->user)
63+
->post(route('discussions.questions.answers', ['questionId' => $this->question->id]))
64+
->seeJsonStructure([
65+
'content',
66+
]);
67+
}
68+
69+
/** @test */
70+
public function it_fails_to_store_answer_on_invalid_question(): void
71+
{
72+
$payload = [
73+
'content' => $this->faker->paragraph,
74+
];
75+
76+
$this->actingAs($this->user)
77+
->post(route('discussions.questions.answers', ['questionId' => 1000]), $payload)
78+
->assertResponseStatus(Response::HTTP_NOT_FOUND);
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Domains\Discussions\Tests\Unit;
4+
5+
use Carbon\Carbon;
6+
use Domains\Accounts\Models\User;
7+
use Domains\Discussions\Database\Factories\AnswerFactory;
8+
use Domains\Discussions\Models\Answer;
9+
use Domains\Discussions\Models\Question;
10+
use Illuminate\Database\Eloquent\SoftDeletingScope;
11+
use Laravel\Lumen\Testing\DatabaseMigrations;
12+
use Tests\TestCase;
13+
14+
class AnswerModelTest extends TestCase
15+
{
16+
use DatabaseMigrations;
17+
18+
private Answer $model;
19+
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->model = AnswerFactory::new()->make();
25+
}
26+
27+
/** @test */
28+
public function it_contains_required_properties(): void
29+
{
30+
self::assertIsInt($this->model->author_id);
31+
self::assertIsInt($this->model->question_id);
32+
self::assertIsString($this->model->content);
33+
self::assertInstanceOf(Carbon::class, $this->model->created_at);
34+
self::assertInstanceOf(Carbon::class, $this->model->updated_at);
35+
}
36+
37+
/** @test */
38+
public function it_uses_correct_table_name(): void
39+
{
40+
self::assertEquals('question_answers', $this->model->getTable());
41+
}
42+
43+
/** @test */
44+
public function it_uses_correct_primary_key(): void
45+
{
46+
self::assertEquals('id', $this->model->getKeyName());
47+
}
48+
49+
/** @test */
50+
public function it_uses_soft_deletes(): void
51+
{
52+
self::assertArrayHasKey(SoftDeletingScope::class, $this->model->getGlobalScopes());
53+
}
54+
55+
/** @test */
56+
public function it_uses_timestamps(): void
57+
{
58+
self::assertTrue($this->model->usesTimestamps());
59+
}
60+
61+
/** @test */
62+
public function it_has_author_relation(): void
63+
{
64+
self::assertInstanceOf(User::class, $this->model->author()->getModel());
65+
}
66+
67+
/** @test */
68+
public function it_has_question_relation(): void
69+
{
70+
self::assertInstanceOf(Question::class, $this->model->question()->getModel());
71+
}
72+
}

domains/Discussions/routes.php

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

3+
use Domains\Discussions\Controllers\AnswersStoreController;
34
use Domains\Discussions\Controllers\QuestionsStoreController;
45
use Illuminate\Support\Facades\Route;
56

@@ -8,3 +9,9 @@
89
'middleware' => 'auth',
910
'uses' => QuestionsStoreController::class,
1011
]);
12+
13+
Route::post('/questions/{questionId}/answers', [
14+
'as' => 'questions.answers',
15+
'middleware' => 'auth',
16+
'uses' => AnswersStoreController::class
17+
]);

0 commit comments

Comments
 (0)