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

Commit

Permalink
Resolves #32 - A guest or an authenticated user can list answers of a…
Browse files Browse the repository at this point in the history
… question (#53)

* #32 gets answers from a question

* #32 fix repeated route name

* #32 code review improvements

* #32 adds second round of code review

* #32 adds minor code reviews

* #32 adds final code review

* #32 removes unneeded index

* #32 edits changelog

Co-authored-by: José Postiga <[email protected]>
  • Loading branch information
ana-lisboa and José Postiga authored Nov 22, 2020
1 parent 25521ea commit 0ea6b54
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ All notable changes to `laravel-portugal/api` will be documented in this file
- A guest or an authenticated user can list questions (#26)
- Guest cannot submit Links for existing author_email (#52)
- An authenticated user can update an answer of a given question (#33)
- A guest or an authenticated user can list answers of a question (#32)

### Changed

Expand Down
47 changes: 47 additions & 0 deletions domains/Discussions/Controllers/AnswersIndexController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Domains\Discussions\Controllers;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Domains\Discussions\Models\Question;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Contracts\Auth\Factory as Auth;
use Domains\Discussions\Resources\AnswerResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class AnswersIndexController extends Controller
{
private Question $question;

public function __construct(Auth $auth, Question $question)
{
if ($auth->guest()) {
$this->middleware('throttle:30,1');
}

$this->question = $question;
}

public function __invoke(Request $request, int $questionId): AnonymousResourceCollection
{
$this->validate($request, [
'author' => ['sometimes', 'integer', 'exists:users,id'],
'created' => ['sometimes', 'array', 'size:2'],
'created.from' => ['required_with:created', 'date'],
'created.to' => ['required_with:created', 'date', 'afterOrEqual:created.from']
]);

$answers = $this->question
->findOrFail($questionId)
->answers()
->when($authorId = $request->input('author'),
static fn(Builder $answers) => $answers->whereAuthorId($authorId))
->when($created = $request->input('created'),
static fn(Builder $answers) => $answers->whereBetween('created_at', [$created['from'], $created['to']]))
->latest()
->simplePaginate(15);

return AnswerResource::collection($answers);
}
}
6 changes: 6 additions & 0 deletions domains/Discussions/Models/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class Question extends Model
Expand All @@ -22,6 +23,11 @@ public function author(): BelongsTo
->withTrashed();
}

public function answers(): HasMany
{
return $this->hasMany(Answer::class);
}

public function scopeFindByAuthorId(Builder $query, int $term): Builder
{
return $query->where('author_id', $term);
Expand Down
28 changes: 28 additions & 0 deletions domains/Discussions/Resources/AnswerResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Domains\Discussions\Resources;

use Domains\Accounts\Resources\UserResource;
use Illuminate\Http\Resources\Json\JsonResource;

class AnswerResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'content' => $this->content,
'question_id' => $this->question_id,
'author_id' => $this->author_id,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'deleted_at' => $this->deleted_at,
'question' => QuestionResource::collection(
$this->whenLoaded('question')
),
'author' => UserResource::collection(
$this->whenLoaded('author')
),
];
}
}
151 changes: 151 additions & 0 deletions domains/Discussions/Tests/Feature/AnswersIndexControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace Domains\Discussions\Tests\Feature;

use Carbon\Carbon;
use Domains\Accounts\Database\Factories\UserFactory;
use Domains\Accounts\Models\User;
use Domains\Discussions\Database\Factories\AnswerFactory;
use Domains\Discussions\Database\Factories\QuestionFactory;
use Domains\Discussions\Models\Answer;
use Domains\Discussions\Models\Question;
use Illuminate\Http\Response;
use Laravel\Lumen\Testing\DatabaseMigrations;
use Tests\TestCase;

class AnswersIndexControllerTest extends TestCase
{
use DatabaseMigrations;

private User $user;
private Question $question;
private Answer $answer;
private Answer $secondAnswer;

protected function setUp(): void
{
parent::setUp();

$this->user = UserFactory::new()->create();

$this->question = QuestionFactory::new([
'author_id' => $this->user->id
])->create();

$this->answer = AnswerFactory::new([
'question_id' => $this->question->id,
'author_id' => $this->user->id,
'created_at' => Carbon::now()->subWeek()->toDateTimeString()
])->create();

$this->secondAnswer = AnswerFactory::new([
'question_id' => $this->question->id,
'created_at' => Carbon::now()->toDateTimeString()
])->create();
}

/** @test */
public function it_gets_paginated_answers_for_a_question(): void
{
$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]))
->seeJsonStructure([
'data' => [
[
'id',
'content',
'question_id',
'author_id',
'created_at',
'updated_at',
'deleted_at'
]
]
])
->seeJsonContains(['id' => $this->answer->id])
->seeJsonContains(['content' => $this->answer->content])
->seeJsonContains(['id' => $this->secondAnswer->id])
->seeJsonContains(['content' => $this->secondAnswer->content]);
}

/** @test * */
public function it_gets_paginated_answers_for_a_question_from_a_particular_author(): void
{
$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id, 'author' => $this->user->id]))
->seeJson(['id' => $this->answer->id])
->dontSeeJson(['id' => $this->secondAnswer->id]);
}

/** @test */
public function it_gets_paginated_answers_for_a_question_from_a_particular_time_frame(): void
{
$aWeekAgo = Carbon::now()->subDays(8);
$yesterday = Carbon::yesterday();

$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]) . '?created[from]=' . $aWeekAgo->format('Y-m-d') . '&created[to]=' . $yesterday->format('Y-m-d'))
->seeJsonContains(['id' => $this->answer->id])
->seeJsonContains(['content' => $this->answer->content])
->seeJsonDoesntContains([
"id" => $this->secondAnswer->id
]);
}

/** @test */
public function it_gets_paginated_answers_for_a_question_from_a_particular_time_frame_and_user(): void
{
$thirdAnswer = AnswerFactory::new([
'question_id' => $this->question->id,
'author_id' => $this->user->id,
'created_at' => Carbon::now()->subWeek()->toDateTimeString()
])->create();

$aWeekAgo = Carbon::now()->subDays(8);
$yesterday = Carbon::yesterday();

$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]) . '?created[from]=' . $aWeekAgo->format('Y-m-d') . '&created[to]=' . $yesterday->format('Y-m-d') . '&author=1')
->seeJsonContains(['id' => $this->answer->id])
->seeJsonContains(['content' => $this->answer->content])
->seeJsonContains(['id' => $thirdAnswer->id])
->seeJsonContains(['content' => $thirdAnswer->content])
->dontSeeJson([
"id" => $this->secondAnswer->id
]);
}

/** @test */
public function it_blocks_guest_for_many_attempts(): void
{
for ($attempt = 0; $attempt < 30; ++$attempt) {
$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]))
->assertResponseStatus(Response::HTTP_OK);
}

$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]))
->assertResponseStatus(Response::HTTP_TOO_MANY_REQUESTS);
}

/** @test */
public function it_not_blocks_authenticated_user_for_many_attempts(): void
{
$this->actingAs($this->user);

for ($attempt = 0; $attempt < 30; ++$attempt) {
$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]));
}

$this->get(route('discussions.questions.answers.list', ['questionId' => $this->question->id]))
->assertResponseStatus(Response::HTTP_OK);
}

/** @test */
public function it_gets_question_and_author_when_loaded(): void
{
$answer = AnswerFactory::new([
'question_id' => $this->question->id,
'author_id' => $this->user->id,
'created_at' => Carbon::now()->subWeek()->toDateTimeString()
])->create()->load('question', 'author');

$this->assertArrayHasKey('author', $answer->relationsToArray());
$this->assertArrayHasKey('question', $answer->relationsToArray());
}
}
6 changes: 6 additions & 0 deletions domains/Discussions/routes.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Domains\Discussions\Controllers\AnswersStoreController;
use Domains\Discussions\Controllers\AnswersIndexController;
use Domains\Discussions\Controllers\QuestionsDeleteController;
use Domains\Discussions\Controllers\QuestionsIndexController;
use Domains\Discussions\Controllers\AnswersUpdateController;
Expand Down Expand Up @@ -45,3 +46,8 @@
'as' => 'questions.view',
'uses' => QuestionsViewController::class,
]);

Route::get('/questions/{questionId}/answers', [
'as' => 'questions.answers.list',
'uses' => AnswersIndexController::class
]);

0 comments on commit 0ea6b54

Please sign in to comment.