From 87c84cfbdfe8651e833be88344ddb16f1f22cb6f Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 01:09:17 +0100 Subject: [PATCH 01/23] create mail for close quiz action --- app/Actions/CloseUserQuizAction.php | 3 ++ app/Events/UserQuizClosed.php | 21 ++++++++++ app/Listeners/SendQuizClosedNotification.php | 20 ++++++++++ app/Mail/QuizClosedMail.php | 40 ++++++++++++++++++++ resources/views/emails/quiz-closed.blade.php | 33 ++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 app/Events/UserQuizClosed.php create mode 100644 app/Listeners/SendQuizClosedNotification.php create mode 100644 app/Mail/QuizClosedMail.php create mode 100644 resources/views/emails/quiz-closed.blade.php diff --git a/app/Actions/CloseUserQuizAction.php b/app/Actions/CloseUserQuizAction.php index 5db6ff98..65c2e4d0 100644 --- a/app/Actions/CloseUserQuizAction.php +++ b/app/Actions/CloseUserQuizAction.php @@ -4,6 +4,7 @@ namespace App\Actions; +use App\Events\UserQuizClosed; use App\Models\UserQuiz; use Carbon\Carbon; @@ -13,5 +14,7 @@ public function execute(UserQuiz $userQuiz): void { $userQuiz->closed_at = Carbon::now(); $userQuiz->save(); + + event(new UserQuizClosed($userQuiz)); } } diff --git a/app/Events/UserQuizClosed.php b/app/Events/UserQuizClosed.php new file mode 100644 index 00000000..b304dbea --- /dev/null +++ b/app/Events/UserQuizClosed.php @@ -0,0 +1,21 @@ +userQuiz; + + Mail::to($quiz->user->email)->send(new QuizClosedMail($quiz)); + } +} diff --git a/app/Mail/QuizClosedMail.php b/app/Mail/QuizClosedMail.php new file mode 100644 index 00000000..85c1f249 --- /dev/null +++ b/app/Mail/QuizClosedMail.php @@ -0,0 +1,40 @@ +userQuiz->quiz->title . " — podsumowanie", + ); + } + + public function content(): Content + { + return new Content( + view: "emails.quiz-closed", + with: [ + "user" => $this->userQuiz->user, + "userQuiz" => $this->userQuiz, + ], + ); + } +} diff --git a/resources/views/emails/quiz-closed.blade.php b/resources/views/emails/quiz-closed.blade.php new file mode 100644 index 00000000..5a5569a1 --- /dev/null +++ b/resources/views/emails/quiz-closed.blade.php @@ -0,0 +1,33 @@ + + + + +
+
+ interns2024b Logo +
+ +
+

Cześć, {{ $user->firstname }}!

+ +

+ Otrzymaliśmy Twoje odpowiedzi na test {{ $userQuiz->quiz->title }}.
+ Twój test jest obecnie sprawdzany, wyślemy powiadomienie na Twoją skrzynkę pocztową, gdy wyniki będą dostępne. +

+ +

Pozdrawiamy,
{{ config('app.name') }}

+ +
+ +
+ © 2024 {{ config('app.name') }}. Wszelkie prawa zastrzeżone. +
+
+
+ From 3c574d71be220033f634ccab7f5a50ad700d086e Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 01:09:43 +0100 Subject: [PATCH 02/23] add queue:listen to makefile --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 5a004854..e9e7573b 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,9 @@ run: stop: @docker compose --file ${DOCKER_COMPOSE_FILE} stop +queue: + @docker compose --file ${DOCKER_COMPOSE_FILE} exec --user "${CURRENT_USER_ID}:${CURRENT_USER_GROUP_ID}" ${DOCKER_COMPOSE_APP_CONTAINER} php artisan queue:listen + restart: stop run shell: From 20a334ba8a4c6369d9ae17f4106b7c3e9c53f670 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 01:22:25 +0100 Subject: [PATCH 03/23] remove duplicate recipes --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index e9e7573b..5a004854 100644 --- a/Makefile +++ b/Makefile @@ -37,9 +37,6 @@ run: stop: @docker compose --file ${DOCKER_COMPOSE_FILE} stop -queue: - @docker compose --file ${DOCKER_COMPOSE_FILE} exec --user "${CURRENT_USER_ID}:${CURRENT_USER_GROUP_ID}" ${DOCKER_COMPOSE_APP_CONTAINER} php artisan queue:listen - restart: stop run shell: From c075f49bc40664394ee41fd430cfd1dbaa902301 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 07:17:37 +0100 Subject: [PATCH 04/23] split quiz controller logic into action files --- app/Actions/CreateUserQuizAction.php | 36 +++++++++++++ app/Actions/LockQuizAction.php | 17 ++++++ app/Actions/UnlockQuizAction.php | 16 ++++++ .../Controllers/QuestionAnswerController.php | 5 +- app/Http/Controllers/QuizController.php | 31 +++++------ .../Controllers/QuizQuestionController.php | 5 +- app/Jobs/CloseUserQuizJob.php | 36 +++++++++++++ app/Listeners/SendQuizClosedNotification.php | 4 +- ...zClosedMail.php => UserQuizClosedMail.php} | 2 +- app/Models/Answer.php | 8 --- app/Models/Question.php | 14 ----- app/Models/Quiz.php | 39 ++------------ app/Models/UserQuiz.php | 5 ++ app/Services/QuizCloneService.php | 54 +++++++++++++++++++ composer.lock | 4 +- tests/Feature/UserQuestionTest.php | 16 +++--- tests/Feature/UserQuizTest.php | 8 ++- .../GetSchoolDataServiceTest.php | 0 .../IsSchoolValidForRegularUsersTest.php | 0 .../QuizUpdateServiceTest.php | 0 20 files changed, 212 insertions(+), 88 deletions(-) create mode 100644 app/Actions/CreateUserQuizAction.php create mode 100644 app/Actions/LockQuizAction.php create mode 100644 app/Actions/UnlockQuizAction.php create mode 100644 app/Jobs/CloseUserQuizJob.php rename app/Mail/{QuizClosedMail.php => UserQuizClosedMail.php} (95%) create mode 100644 app/Services/QuizCloneService.php rename tests/{Feature => Unit}/GetSchoolDataServiceTest.php (100%) rename tests/{Feature => Unit}/IsSchoolValidForRegularUsersTest.php (100%) rename tests/{Feature => Unit}/QuizUpdateServiceTest.php (100%) diff --git a/app/Actions/CreateUserQuizAction.php b/app/Actions/CreateUserQuizAction.php new file mode 100644 index 00000000..494a20fe --- /dev/null +++ b/app/Actions/CreateUserQuizAction.php @@ -0,0 +1,36 @@ +closed_at = $quiz->closeAt; + $userQuiz->quiz()->associate($quiz); + $userQuiz->user()->associate($user); + $userQuiz->save(); + + foreach ($quiz->questions as $question) { + $userQuestion = new UserQuestion(); + $userQuestion->userQuiz()->associate($userQuiz); + $userQuestion->question()->associate($question); + $userQuestion->save(); + } + + if ($quiz->isClosingToday()) { + CloseUserQuizJob::dispatch($quiz)->delay($quiz->closeAt); + } + + return $userQuiz; + } +} diff --git a/app/Actions/LockQuizAction.php b/app/Actions/LockQuizAction.php new file mode 100644 index 00000000..a9392c03 --- /dev/null +++ b/app/Actions/LockQuizAction.php @@ -0,0 +1,17 @@ +locked_at = Carbon::now(); + $quiz->save(); + } +} diff --git a/app/Actions/UnlockQuizAction.php b/app/Actions/UnlockQuizAction.php new file mode 100644 index 00000000..3a4131a6 --- /dev/null +++ b/app/Actions/UnlockQuizAction.php @@ -0,0 +1,16 @@ +locked_at = null; + $quiz->save(); + } +} diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php index e7d41ae7..355a43fa 100644 --- a/app/Http/Controllers/QuestionAnswerController.php +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -7,6 +7,7 @@ use App\Http\Requests\AnswerRequest; use App\Models\Answer; use App\Models\Question; +use App\Services\QuizCloneService; use Illuminate\Http\RedirectResponse; class QuestionAnswerController extends Controller @@ -52,9 +53,9 @@ public function destroy(Answer $answer): RedirectResponse return redirect()->back(); } - public function clone(Answer $answer, Question $question): RedirectResponse + public function clone(QuizCloneService $service, Answer $answer, Question $question): RedirectResponse { - $answer->cloneTo($question); + $service->cloneAnswer($answer, $question); return redirect() ->back() diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index 5f46f53c..3630ea64 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -4,11 +4,16 @@ namespace App\Http\Controllers; +use App\Actions\AssignToQuizAction; +use App\Actions\CreateUserQuizAction; +use App\Actions\LockQuizAction; +use App\Actions\UnlockQuizAction; use App\Helpers\SortHelper; use App\Http\Requests\QuizRequest; use App\Http\Requests\UpdateQuizRequest; use App\Http\Resources\QuizResource; use App\Models\Quiz; +use App\Services\QuizCloneService; use App\Services\QuizUpdateService; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; @@ -17,6 +22,7 @@ use Inertia\Inertia; use Inertia\Response; +use function collect; use function redirect; class QuizController extends Controller @@ -60,48 +66,43 @@ public function destroy(Quiz $quiz): RedirectResponse return redirect()->back(); } - public function clone(Quiz $quiz): RedirectResponse + public function clone(QuizCloneService $service, Quiz $quiz): RedirectResponse { - $quiz->clone(); + $service->cloneQuiz($quiz); return redirect() ->back() ->with("status", "Test został skopiowany"); } - public function lock(Quiz $quiz): RedirectResponse + public function lock(LockQuizAction $action, Quiz $quiz): RedirectResponse { - $quiz->locked_at = Carbon::now(); - $quiz->save(); + $action->execute($quiz); return redirect() ->back() ->with("status", "Test oznaczony jako gotowy do publikacji"); } - public function unlock(Quiz $quiz): RedirectResponse + public function unlock(UnlockQuizAction $action, Quiz $quiz): RedirectResponse { - $quiz->locked_at = null; - $quiz->save(); + $action->execute($quiz); return redirect() ->back() ->with("status", "Publikacja testu została wycofana"); } - public function createUserQuiz(Request $request, Quiz $quiz): RedirectResponse + public function createUserQuiz(CreateUserQuizAction $action, Request $request, Quiz $quiz): RedirectResponse { - $user = $request->user(); - $userQuiz = $quiz->createUserQuiz($user); + $userQuiz = $action->execute($quiz, $request->user()); return redirect("/quizzes/{$userQuiz->id}/"); } - public function assign(Request $request, Quiz $quiz): RedirectResponse + public function assign(AssignToQuizAction $action, Request $request, Quiz $quiz): RedirectResponse { - $user = $request->user(); - $quiz->assignedUsers()->attach($user); - $quiz->save(); + $action->execute($quiz, collect([$request->user()->id])); return redirect() ->back() diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php index 2f23ebf2..2344d877 100644 --- a/app/Http/Controllers/QuizQuestionController.php +++ b/app/Http/Controllers/QuizQuestionController.php @@ -7,6 +7,7 @@ use App\Http\Requests\QuestionRequest; use App\Models\Question; use App\Models\Quiz; +use App\Services\QuizCloneService; use Illuminate\Http\RedirectResponse; class QuizQuestionController extends Controller @@ -35,9 +36,9 @@ public function destroy(Question $question): RedirectResponse return redirect()->back(); } - public function clone(Question $question, Quiz $quiz): RedirectResponse + public function clone(QuizCloneService $service, Question $question, Quiz $quiz): RedirectResponse { - $question->cloneTo($quiz); + $service->cloneQuestion($question, $quiz); return redirect() ->back() diff --git a/app/Jobs/CloseUserQuizJob.php b/app/Jobs/CloseUserQuizJob.php new file mode 100644 index 00000000..a9cb59bb --- /dev/null +++ b/app/Jobs/CloseUserQuizJob.php @@ -0,0 +1,36 @@ +quiz->userQuizzes as $userQuiz) { + if (!$userQuiz->wasClosedManually()) { + event(new UserQuizClosed($userQuiz)); + } + } + } +} diff --git a/app/Listeners/SendQuizClosedNotification.php b/app/Listeners/SendQuizClosedNotification.php index 2e9c7874..3d3d9bef 100644 --- a/app/Listeners/SendQuizClosedNotification.php +++ b/app/Listeners/SendQuizClosedNotification.php @@ -5,7 +5,7 @@ namespace App\Listeners; use App\Events\UserQuizClosed; -use App\Mail\QuizClosedMail; +use App\Mail\UserQuizClosedMail; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Mail; @@ -15,6 +15,6 @@ public function handle(UserQuizClosed $event): void { $quiz = $event->userQuiz; - Mail::to($quiz->user->email)->send(new QuizClosedMail($quiz)); + Mail::to($quiz->user->email)->send(new UserQuizClosedMail($quiz)); } } diff --git a/app/Mail/QuizClosedMail.php b/app/Mail/UserQuizClosedMail.php similarity index 95% rename from app/Mail/QuizClosedMail.php rename to app/Mail/UserQuizClosedMail.php index 85c1f249..09693818 100644 --- a/app/Mail/QuizClosedMail.php +++ b/app/Mail/UserQuizClosedMail.php @@ -11,7 +11,7 @@ use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; -class QuizClosedMail extends Mailable +class UserQuizClosedMail extends Mailable { use Queueable; use SerializesModels; diff --git a/app/Models/Answer.php b/app/Models/Answer.php index f4f7296f..143bec6d 100644 --- a/app/Models/Answer.php +++ b/app/Models/Answer.php @@ -42,12 +42,4 @@ public function isCorrect(): Attribute { return Attribute::get(fn(): bool => $this->question->correctAnswer()->is($this)); } - - public function cloneTo(Question $question): self - { - $clone = $this->replicate(); - $clone->question()->associate($question)->save(); - - return $clone; - } } diff --git a/app/Models/Question.php b/app/Models/Question.php index 808f3647..de9c227a 100644 --- a/app/Models/Question.php +++ b/app/Models/Question.php @@ -60,19 +60,5 @@ public function hasCorrectAnswer(): Attribute public function cloneTo(Quiz $quiz): self { - $questionCopy = $this->replicate(); - $questionCopy->quiz()->associate($quiz)->save(); - - foreach ($this->answers as $answer) { - $answerCopy = $answer->cloneTo($questionCopy); - - if ($answer->isCorrect) { - $questionCopy->correctAnswer()->associate($answerCopy); - } - } - - $questionCopy->save(); - - return $questionCopy; } } diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 87d4bdd2..22707ada 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -108,40 +108,6 @@ public function closeAt(): Attribute return Attribute::get(fn(): ?Carbon => $this->isReadyToBePublished() ? $this->scheduled_at->copy()->addMinutes($this->duration) : null); } - public function clone(): self - { - $quizCopy = $this->replicate(); - $quizCopy->title = $quizCopy->title . " - kopia"; - $quizCopy->locked_at = null; - $quizCopy->duration = null; - $quizCopy->scheduled_at = null; - $quizCopy->save(); - - foreach ($this->questions as $question) { - $question->cloneTo($quizCopy); - } - - return $quizCopy; - } - - public function createUserQuiz(User $user): UserQuiz - { - $userQuiz = new UserQuiz(); - $userQuiz->closed_at = $this->closeAt; - $userQuiz->quiz()->associate($this); - $userQuiz->user()->associate($user); - $userQuiz->save(); - - foreach ($this->questions as $question) { - $userQuestion = new UserQuestion(); - $userQuestion->userQuiz()->associate($userQuiz); - $userQuestion->question()->associate($question); - $userQuestion->save(); - } - - return $userQuiz; - } - public function isReadyToBePublished(): bool { return $this->scheduled_at !== null && $this->duration !== null && $this->allQuestionsHaveCorrectAnswer(); @@ -152,6 +118,11 @@ public function hasUserQuizzesFrom(User $user): bool return $this->userQuizzes->where("user_id", $user->id)->isNotEmpty(); } + public function isClosingToday(): bool + { + return $this->isLocked && $this->closeAt->isFuture() && $this->closeAt->isToday(); + } + protected function allQuestionsHaveCorrectAnswer(): bool { return $this->questions->every(fn(Question $question): bool => $question->hasCorrectAnswer); diff --git a/app/Models/UserQuiz.php b/app/Models/UserQuiz.php index e54473c1..b6f21a1e 100644 --- a/app/Models/UserQuiz.php +++ b/app/Models/UserQuiz.php @@ -50,6 +50,11 @@ public function isClosed(): Attribute return Attribute::get(fn(): bool => $this->closed_at <= Carbon::now()); } + public function wasClosedManually(): bool + { + return $this->closed_at !== null; + } + public function points(): Attribute { return Attribute::get(function (): int { diff --git a/app/Services/QuizCloneService.php b/app/Services/QuizCloneService.php new file mode 100644 index 00000000..394a6eb7 --- /dev/null +++ b/app/Services/QuizCloneService.php @@ -0,0 +1,54 @@ +replicate(); + $quizCopy->title = $quizCopy->title . " - kopia"; + $quizCopy->locked_at = null; + $quizCopy->duration = null; + $quizCopy->scheduled_at = null; + $quizCopy->save(); + + foreach ($quiz->questions as $question) { + $this->cloneQuestion($question, $quizCopy); + } + + return $quizCopy; + } + + public function cloneQuestion(Question $question, Quiz $target): Question + { + $questionCopy = $question->replicate(); + $questionCopy->quiz()->associate($target)->save(); + + foreach ($question->answers as $answer) { + $answerCopy = $this->cloneAnswer($answer, $questionCopy); + + if ($answer->isCorrect) { + $questionCopy->correctAnswer()->associate($answerCopy); + } + } + + $questionCopy->save(); + + return $questionCopy; + } + + public function cloneAnswer(Answer $answer, Question $target): Answer + { + $clone = $answer->replicate(); + $clone->question()->associate($target)->save(); + + return $clone; + } +} diff --git a/composer.lock b/composer.lock index 2fe4ef9d..d716b04d 100644 --- a/composer.lock +++ b/composer.lock @@ -9940,13 +9940,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.3.4", "ext-pdo": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/tests/Feature/UserQuestionTest.php b/tests/Feature/UserQuestionTest.php index f3b51f53..14c6ec08 100644 --- a/tests/Feature/UserQuestionTest.php +++ b/tests/Feature/UserQuestionTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature; +use App\Actions\CreateUserQuizAction; use App\Models\Answer; use App\Models\User; use App\Models\UserQuiz; @@ -16,18 +17,21 @@ class UserQuestionTest extends TestCase use RefreshDatabase; protected User $user; + protected CreateUserQuizAction $createUserQuiz; protected function setUp(): void { parent::setUp(); - UserQuiz::truncate(); + UserQuiz::query()->truncate(); + + $this->createUserQuiz = new CreateUserQuizAction(); $this->user = User::factory()->create(); } public function testUserCanAnswerQuestion(): void { $answer = Answer::factory()->locked()->create(); - $userQuiz = $answer->question->quiz->createUserQuiz($this->user); + $userQuiz = $this->createUserQuiz->execute($answer->question->quiz, $this->user); $userQuestion = $userQuiz->userQuestions[0]; $this->actingAs($this->user) @@ -54,7 +58,7 @@ public function testUserCannotAnswerQuestionThatIsNotTheirs(): void { $user = User::factory()->create(); $answer = Answer::factory()->locked()->create(); - $userQuiz = $answer->question->quiz->createUserQuiz($user); + $userQuiz = $this->createUserQuiz->execute($answer->question->quiz, $user); $userQuestion = $userQuiz->userQuestions[0]; $this->actingAs($this->user) @@ -71,7 +75,7 @@ public function testUserCannotAnswerQuestionThatIsNotTheirs(): void public function testUserCannotAnswerQuestionThatBelongsToClosedUserQuiz(): void { $answer = Answer::factory()->locked()->create(); - $userQuiz = $answer->question->quiz->createUserQuiz($this->user); + $userQuiz = $this->createUserQuiz->execute($answer->question->quiz, $this->user); $userQuestion = $userQuiz->userQuestions[0]; $userQuiz->closed_at = Carbon::now(); @@ -91,7 +95,7 @@ public function testUserCannotAnswerQuestionThatBelongsToClosedUserQuiz(): void public function testUserCannotAnswerQuestionWithAnswerThatNotExist(): void { $answer = Answer::factory()->locked()->create(); - $userQuiz = $answer->question->quiz->createUserQuiz($this->user); + $userQuiz = $this->createUserQuiz->execute($answer->question->quiz, $this->user); $userQuestion = $userQuiz->userQuestions[0]; $this->actingAs($this->user) @@ -108,7 +112,7 @@ public function testUserCannotAnswerQuestionWithAnswerThatNotExist(): void public function testUserCannotAnswerQuestionWithAnswerNotAssignedToIt(): void { $answer = Answer::factory()->locked()->create(); - $userQuiz = $answer->question->quiz->createUserQuiz($this->user); + $userQuiz = $this->createUserQuiz->execute($answer->question->quiz, $this->user); $userQuestion = $userQuiz->userQuestions[0]; $answer1 = Answer::factory()->locked()->create(); diff --git a/tests/Feature/UserQuizTest.php b/tests/Feature/UserQuizTest.php index 95a27067..da53dac1 100644 --- a/tests/Feature/UserQuizTest.php +++ b/tests/Feature/UserQuizTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature; +use App\Actions\CreateUserQuizAction; use App\Models\Answer; use App\Models\Question; use App\Models\Quiz; @@ -19,11 +20,13 @@ class UserQuizTest extends TestCase use RefreshDatabase; protected User $user; + protected CreateUserQuizAction $createUserQuiz; protected function setUp(): void { parent::setUp(); + $this->createUserQuiz = new CreateUserQuizAction(); $this->user = User::factory()->create(); } @@ -38,7 +41,7 @@ public function testUserCanViewSingleUserQuiz(): void $question->save(); } - $userQuiz = $quiz->createUserQuiz($this->user); + $userQuiz = $this->createUserQuiz->execute($quiz, $this->user); $this->assertDatabaseCount("quizzes", 1); $this->assertDatabaseCount("questions", 2); @@ -76,7 +79,8 @@ public function testUserCanSeeUserQuizResult(): void $quiz->save(); Question::factory()->count(2)->create(["quiz_id" => $quiz->id]); - $userQuiz = $quiz->createUserQuiz($this->user); + + $userQuiz = $this->createUserQuiz->execute($quiz, $this->user); $userQuiz->closed_at = Carbon::now(); $userQuiz->save(); diff --git a/tests/Feature/GetSchoolDataServiceTest.php b/tests/Unit/GetSchoolDataServiceTest.php similarity index 100% rename from tests/Feature/GetSchoolDataServiceTest.php rename to tests/Unit/GetSchoolDataServiceTest.php diff --git a/tests/Feature/IsSchoolValidForRegularUsersTest.php b/tests/Unit/IsSchoolValidForRegularUsersTest.php similarity index 100% rename from tests/Feature/IsSchoolValidForRegularUsersTest.php rename to tests/Unit/IsSchoolValidForRegularUsersTest.php diff --git a/tests/Feature/QuizUpdateServiceTest.php b/tests/Unit/QuizUpdateServiceTest.php similarity index 100% rename from tests/Feature/QuizUpdateServiceTest.php rename to tests/Unit/QuizUpdateServiceTest.php From 5eeeebaca790c3e97b68baf129a4bb327c254d29 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 10:02:49 +0100 Subject: [PATCH 05/23] add tests for actions --- app/Actions/AssignToQuizAction.php | 4 +- app/Actions/CreateUserQuestionAction.php | 22 +++++ app/Actions/CreateUserQuizAction.php | 10 +-- ...zAction.php => UnassignFromQuizAction.php} | 3 +- app/Http/Controllers/InviteController.php | 4 +- database/factories/QuizFactory.php | 9 ++ database/seeders/PublishedQuizSeeder.php | 35 ++++++++ database/seeders/UserQuizSeeder.php | 36 ++------ tests/Feature/RankingTest.php | 10 +-- tests/Feature/UserQuestionTest.php | 3 +- tests/Feature/UserQuizTest.php | 3 +- tests/Unit/AssignToQuizActionTest.php | 58 ++++++++++++ tests/Unit/CloseUserQuizActionTest.php | 45 ++++++++++ tests/Unit/CreateUserQuestionActionTest.php | 40 +++++++++ tests/Unit/CreateUserQuizActionTest.php | 88 +++++++++++++++++++ tests/Unit/LockQuizActionTest.php | 43 +++++++++ tests/Unit/PublishQuizRankingActionTest.php | 43 +++++++++ tests/Unit/RegonRuleTest.php | 2 +- tests/Unit/SortHelperTest.php | 2 +- tests/Unit/UnassignFromQuizActionTest.php | 61 +++++++++++++ tests/Unit/UnlockQuizActionTest.php | 35 ++++++++ tests/Unit/UnpublishQuizRankingActionTest.php | 35 ++++++++ 22 files changed, 542 insertions(+), 49 deletions(-) create mode 100644 app/Actions/CreateUserQuestionAction.php rename app/Actions/{UnassignToQuizAction.php => UnassignFromQuizAction.php} (94%) create mode 100644 database/seeders/PublishedQuizSeeder.php create mode 100644 tests/Unit/AssignToQuizActionTest.php create mode 100644 tests/Unit/CloseUserQuizActionTest.php create mode 100644 tests/Unit/CreateUserQuestionActionTest.php create mode 100644 tests/Unit/CreateUserQuizActionTest.php create mode 100644 tests/Unit/LockQuizActionTest.php create mode 100644 tests/Unit/PublishQuizRankingActionTest.php create mode 100644 tests/Unit/UnassignFromQuizActionTest.php create mode 100644 tests/Unit/UnlockQuizActionTest.php create mode 100644 tests/Unit/UnpublishQuizRankingActionTest.php diff --git a/app/Actions/AssignToQuizAction.php b/app/Actions/AssignToQuizAction.php index 34c20f3a..6995d7c1 100644 --- a/app/Actions/AssignToQuizAction.php +++ b/app/Actions/AssignToQuizAction.php @@ -11,11 +11,11 @@ class AssignToQuizAction { /** - * @param Collection $users + * @param Collection $users */ public function execute(Quiz $quiz, Collection $users): void { - $assignedUsers = $quiz->assignedUsers; + $assignedUsers = $quiz->assignedUsers()->get(); $users = User::query()->whereIn("id", $users)->get(); $users = $users->filter(fn(User $user) => !$assignedUsers->contains($user)); diff --git a/app/Actions/CreateUserQuestionAction.php b/app/Actions/CreateUserQuestionAction.php new file mode 100644 index 00000000..93ac69f2 --- /dev/null +++ b/app/Actions/CreateUserQuestionAction.php @@ -0,0 +1,22 @@ +userQuiz()->associate($userQuiz); + $userQuestion->question()->associate($question); + $userQuestion->save(); + + return $userQuestion; + } +} diff --git a/app/Actions/CreateUserQuizAction.php b/app/Actions/CreateUserQuizAction.php index 494a20fe..041f93ab 100644 --- a/app/Actions/CreateUserQuizAction.php +++ b/app/Actions/CreateUserQuizAction.php @@ -7,11 +7,14 @@ use App\Jobs\CloseUserQuizJob; use App\Models\Quiz; use App\Models\User; -use App\Models\UserQuestion; use App\Models\UserQuiz; class CreateUserQuizAction { + public function __construct( + protected CreateUserQuestionAction $action, + ) {} + public function execute(Quiz $quiz, User $user): UserQuiz { $userQuiz = new UserQuiz(); @@ -21,10 +24,7 @@ public function execute(Quiz $quiz, User $user): UserQuiz $userQuiz->save(); foreach ($quiz->questions as $question) { - $userQuestion = new UserQuestion(); - $userQuestion->userQuiz()->associate($userQuiz); - $userQuestion->question()->associate($question); - $userQuestion->save(); + $this->action->execute($question, $userQuiz); } if ($quiz->isClosingToday()) { diff --git a/app/Actions/UnassignToQuizAction.php b/app/Actions/UnassignFromQuizAction.php similarity index 94% rename from app/Actions/UnassignToQuizAction.php rename to app/Actions/UnassignFromQuizAction.php index d17a2748..56bf2408 100644 --- a/app/Actions/UnassignToQuizAction.php +++ b/app/Actions/UnassignFromQuizAction.php @@ -8,7 +8,7 @@ use App\Models\User; use Illuminate\Support\Collection; -class UnassignToQuizAction +class UnassignFromQuizAction { /** * @param Collection $users @@ -17,6 +17,7 @@ public function execute(Quiz $quiz, Collection $users): void { $assignedUsers = $quiz->assignedUsers; $users = User::query()->whereIn("id", $users)->get(); + $users = $users->filter(fn(User $user) => $assignedUsers->contains($user)); $quiz->assignedUsers()->detach($users); } diff --git a/app/Http/Controllers/InviteController.php b/app/Http/Controllers/InviteController.php index f9e8687e..19c128bd 100644 --- a/app/Http/Controllers/InviteController.php +++ b/app/Http/Controllers/InviteController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers; use App\Actions\AssignToQuizAction; -use App\Actions\UnassignToQuizAction; +use App\Actions\UnassignFromQuizAction; use App\Helpers\SortHelper; use App\Http\Requests\InviteQuizRequest; use App\Http\Resources\QuizResource; @@ -51,7 +51,7 @@ public function assign(Quiz $quiz, InviteQuizRequest $request, AssignToQuizActio ->with("status", "Użytkownicy zostali przypisani do quizu."); } - public function unassign(Quiz $quiz, InviteQuizRequest $request, UnassignToQuizAction $unassignAction): RedirectResponse + public function unassign(Quiz $quiz, InviteQuizRequest $request, UnassignFromQuizAction $unassignAction): RedirectResponse { $this->authorize("invite", $quiz); diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index 166a0e55..1313560b 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -37,4 +37,13 @@ public function published(): static "ranking_published_at" => null, ]); } + + public function withRanking(): static + { + return $this->state(fn(array $attributes): array => [ + "scheduled_at" => Carbon::now()->subMinutes(30), + "locked_at" => Carbon::now()->subMinutes(15), + "ranking_published_at" => Carbon::now(), + ]); + } } diff --git a/database/seeders/PublishedQuizSeeder.php b/database/seeders/PublishedQuizSeeder.php new file mode 100644 index 00000000..e933be11 --- /dev/null +++ b/database/seeders/PublishedQuizSeeder.php @@ -0,0 +1,35 @@ +has( + Question::factory() + ->count(5) + ->has(Answer::factory()->count(4)), + ) + ->published() + ->create(); + + foreach ($quiz->questions as $question) { + $answers = $question->answers; + + if ($answers->isNotEmpty()) { + $correctAnswer = $answers->random(); + $question->correct_answer_id = $correctAnswer->id; + $question->save(); + } + } + } +} diff --git a/database/seeders/UserQuizSeeder.php b/database/seeders/UserQuizSeeder.php index a65fabdc..a1e84b51 100644 --- a/database/seeders/UserQuizSeeder.php +++ b/database/seeders/UserQuizSeeder.php @@ -4,8 +4,6 @@ namespace Database\Seeders; -use App\Models\Answer; -use App\Models\Question; use App\Models\Quiz; use App\Models\User; use App\Models\UserQuestion; @@ -14,46 +12,26 @@ class UserQuizSeeder extends Seeder { - public Quiz $quiz; - public function run(): void { $user1 = User::factory()->create(); $user2 = User::factory()->create(); - $this->quiz = Quiz::factory() - ->has( - Question::factory() - ->count(5) - ->has( - Answer::factory()->count(4), - ), - ) - ->published() - ->create(); - - foreach ($this->quiz->questions as $question) { - $answers = $question->answers; - - if ($answers->isNotEmpty()) { - $correctAnswer = $answers->random(); - $question->correct_answer_id = $correctAnswer->id; - $question->save(); - } - } + $this->call([PublishedQuizSeeder::class]); + $quiz = Quiz::query()->firstOrFail(); - $this->createUserQuizForUser($user1, null); - $this->createUserQuizForUser($user2, null); + self::createUserQuizForUser($quiz, $user1, null); + self::createUserQuizForUser($quiz, $user2, null); } - public function createUserQuizForUser(User $user, ?int $correctAnswersCount): void + public static function createUserQuizForUser(Quiz $quiz, User $user, ?int $correctAnswersCount): void { $userQuiz = UserQuiz::factory() - ->for($this->quiz) + ->for($quiz) ->for($user) ->create(); - $questions = $this->quiz->questions; + $questions = $quiz->questions; if ($correctAnswersCount === null) { $correctAnswersCount = rand(0, $questions->count()); diff --git a/tests/Feature/RankingTest.php b/tests/Feature/RankingTest.php index 0731294f..f38b5fa7 100644 --- a/tests/Feature/RankingTest.php +++ b/tests/Feature/RankingTest.php @@ -17,7 +17,6 @@ class RankingTest extends TestCase use RefreshDatabase; protected User $user; - protected UserQuizSeeder $seeder; protected User $admin; protected Quiz $quiz; protected Quiz $unlockedQuiz; @@ -26,14 +25,13 @@ protected function setUp(): void { parent::setUp(); - $this->seeder = new UserQuizSeeder(); - $this->seeder->run(); + $this->seed(UserQuizSeeder::class); $this->user = User::factory()->create(); $this->admin = User::factory()->admin()->create(); - $this->quiz = $this->seeder->quiz; - $this->seeder->createUserQuizForUser($this->user, 2); + $this->quiz = Quiz::query()->firstOrFail(); + UserQuizSeeder::createUserQuizForUser($this->quiz, $this->user, 2); $this->unlockedQuiz = Quiz::factory()->create(); } @@ -51,7 +49,7 @@ public function testUserHasPointsInAnsweredQuiz(): void public function testUserPointsAreCalculatedCorrectly(): void { $user2 = User::factory()->create(); - $this->seeder->createUserQuizForUser($user2, 3); + UserQuizSeeder::createUserQuizForUser($this->quiz, $user2, 3); $userQuiz1 = UserQuiz::where("user_id", $this->user->id) ->where("quiz_id", $this->quiz->id) diff --git a/tests/Feature/UserQuestionTest.php b/tests/Feature/UserQuestionTest.php index 14c6ec08..812dc887 100644 --- a/tests/Feature/UserQuestionTest.php +++ b/tests/Feature/UserQuestionTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature; +use App\Actions\CreateUserQuestionAction; use App\Actions\CreateUserQuizAction; use App\Models\Answer; use App\Models\User; @@ -24,7 +25,7 @@ protected function setUp(): void parent::setUp(); UserQuiz::query()->truncate(); - $this->createUserQuiz = new CreateUserQuizAction(); + $this->createUserQuiz = new CreateUserQuizAction(new CreateUserQuestionAction()); $this->user = User::factory()->create(); } diff --git a/tests/Feature/UserQuizTest.php b/tests/Feature/UserQuizTest.php index da53dac1..10f7b838 100644 --- a/tests/Feature/UserQuizTest.php +++ b/tests/Feature/UserQuizTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature; +use App\Actions\CreateUserQuestionAction; use App\Actions\CreateUserQuizAction; use App\Models\Answer; use App\Models\Question; @@ -26,7 +27,7 @@ protected function setUp(): void { parent::setUp(); - $this->createUserQuiz = new CreateUserQuizAction(); + $this->createUserQuiz = new CreateUserQuizAction(new CreateUserQuestionAction()); $this->user = User::factory()->create(); } diff --git a/tests/Unit/AssignToQuizActionTest.php b/tests/Unit/AssignToQuizActionTest.php new file mode 100644 index 00000000..008dabdc --- /dev/null +++ b/tests/Unit/AssignToQuizActionTest.php @@ -0,0 +1,58 @@ +action = new AssignToQuizAction(); + } + + public function testUsersAreAssignedToQuiz(): void + { + User::factory()->count(10)->create(); + $quiz = Quiz::factory()->create(); + + $this->assertEquals(0, $quiz->assignedUsers()->count()); + $this->action->execute($quiz, User::all()->pluck("id")); + $this->assertEquals(10, $quiz->assignedUsers()->count()); + } + + public function testAlreadyAssignedUsersAreSkipped(): void + { + User::factory()->count(10)->create(); + $quiz = Quiz::factory()->create(); + + $this->action->execute($quiz, User::all()->pluck("id")); + $this->assertEquals(10, $quiz->assignedUsers()->count()); + + $this->action->execute($quiz, User::all()->pluck("id")); + $this->assertEquals(10, $quiz->assignedUsers()->count()); + } + + public function testUnknownUsersAreSkipped(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + + $this->action->execute($quiz, collect([$user->id, 2, 3, 4, 5])); + $this->assertEquals(1, $quiz->assignedUsers()->count()); + } +} diff --git a/tests/Unit/CloseUserQuizActionTest.php b/tests/Unit/CloseUserQuizActionTest.php new file mode 100644 index 00000000..fdca77a5 --- /dev/null +++ b/tests/Unit/CloseUserQuizActionTest.php @@ -0,0 +1,45 @@ +action = new CloseUserQuizAction(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + + public function testActionIsClosingUserQuiz(): void + { + $quiz = UserQuiz::factory()->create(); + + $this->action->execute($quiz); + + Event::assertDispatched(UserQuizClosed::class); + $this->assertDatabaseHas("user_quizzes", ["id" => $quiz->id, "closed_at" => "2024-10-11:08:00"]); + } +} diff --git a/tests/Unit/CreateUserQuestionActionTest.php b/tests/Unit/CreateUserQuestionActionTest.php new file mode 100644 index 00000000..fd416e1b --- /dev/null +++ b/tests/Unit/CreateUserQuestionActionTest.php @@ -0,0 +1,40 @@ +action = new CreateUserQuestionAction(); + $this->userQuiz = UserQuiz::factory()->create(); + $this->question = Question::factory()->create(); + } + + public function testUserQuestionIsCreated(): void + { + $userQuestion = $this->action->execute($this->question, $this->userQuiz); + + $this->assertDatabaseHas("user_questions", [ + "id" => $userQuestion->id, + "user_quiz_id" => $this->userQuiz->id, + "question_id" => $this->question->id, + ]); + } +} diff --git a/tests/Unit/CreateUserQuizActionTest.php b/tests/Unit/CreateUserQuizActionTest.php new file mode 100644 index 00000000..cedf7561 --- /dev/null +++ b/tests/Unit/CreateUserQuizActionTest.php @@ -0,0 +1,88 @@ +seed(PublishedQuizSeeder::class); + $this->action = new CreateUserQuizAction(new CreateUserQuestionAction()); + $this->quiz = Quiz::query()->firstOrFail(); + $this->user = User::factory()->create(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + + public function testUserQuizIsCreated(): void + { + $userQuiz = $this->action->execute($this->quiz, $this->user); + + $this->assertDatabaseHas("user_quizzes", [ + "id" => $userQuiz->id, + "closed_at" => $this->quiz->closeAt, + "quiz_id" => $this->quiz->id, + "user_id" => $this->user->id, + ]); + + $this->assertDatabaseCount("user_questions", 5); + } + + public function testCloseUserQuizJobIsDispatchedIfQuizIsClosingToday(): void + { + $this->quiz->scheduled_at = Carbon::now()->addHour(); + $this->quiz->save(); + + $this->action->execute($this->quiz, $this->user); + + Queue::assertPushed(CloseUserQuizJob::class); + } + + public function testCloseUserQuizJobIsNotDispatchedIfQuizIsAlreadyClosed(): void + { + $this->quiz->scheduled_at = Carbon::now()->subHours(3); + $this->quiz->save(); + + $this->action->execute($this->quiz, $this->user); + + Queue::assertNotPushed(CloseUserQuizJob::class); + } + + public function testCloseUserQuizJobIsNotDispatchedIfQuizIsNotClosingToday(): void + { + $this->quiz->scheduled_at = Carbon::now()->addDay(); + $this->quiz->save(); + + $this->action->execute($this->quiz, $this->user); + + Queue::assertNotPushed(CloseUserQuizJob::class); + } +} diff --git a/tests/Unit/LockQuizActionTest.php b/tests/Unit/LockQuizActionTest.php new file mode 100644 index 00000000..00f10f94 --- /dev/null +++ b/tests/Unit/LockQuizActionTest.php @@ -0,0 +1,43 @@ +action = new LockQuizAction(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + + public function testActionIsLockingQuiz(): void + { + $quiz = Quiz::factory()->create(); + $this->action->execute($quiz); + + $this->assertDatabaseHas("quizzes", [ + "id" => $quiz->id, + "locked_at" => "2024-10-11:08:00", + ]); + } +} diff --git a/tests/Unit/PublishQuizRankingActionTest.php b/tests/Unit/PublishQuizRankingActionTest.php new file mode 100644 index 00000000..2983cf3b --- /dev/null +++ b/tests/Unit/PublishQuizRankingActionTest.php @@ -0,0 +1,43 @@ +action = new PublishQuizRankingAction(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + + public function testActionIsPublishingQuiz(): void + { + $quiz = Quiz::factory()->create(); + $this->action->execute($quiz); + + $this->assertDatabaseHas("quizzes", [ + "id" => $quiz->id, + "ranking_published_at" => "2024-10-11:08:00", + ]); + } +} diff --git a/tests/Unit/RegonRuleTest.php b/tests/Unit/RegonRuleTest.php index a3efaeac..50bb5802 100644 --- a/tests/Unit/RegonRuleTest.php +++ b/tests/Unit/RegonRuleTest.php @@ -6,7 +6,7 @@ use App\Rules\Regon; use Illuminate\Support\Facades\Lang; -use PHPUnit\Framework\TestCase; +use Tests\TestCase; class RegonRuleTest extends TestCase { diff --git a/tests/Unit/SortHelperTest.php b/tests/Unit/SortHelperTest.php index 6e03c326..0d22a945 100644 --- a/tests/Unit/SortHelperTest.php +++ b/tests/Unit/SortHelperTest.php @@ -12,8 +12,8 @@ use Illuminate\Support\Facades\Lang; use Mockery; use Mockery\MockInterface; -use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\HttpException; +use Tests\TestCase; class SortHelperTest extends TestCase { diff --git a/tests/Unit/UnassignFromQuizActionTest.php b/tests/Unit/UnassignFromQuizActionTest.php new file mode 100644 index 00000000..fc0d6b4d --- /dev/null +++ b/tests/Unit/UnassignFromQuizActionTest.php @@ -0,0 +1,61 @@ +action = new UnassignFromQuizAction(); + } + + public function testUsersAreUnassignedFromQuiz(): void + { + User::factory()->count(10)->create(); + $quiz = Quiz::factory()->create(); + $quiz->assignedUsers()->attach(User::all()); + + $this->assertEquals(10, $quiz->assignedUsers()->count()); + $this->action->execute($quiz, User::all()->pluck("id")); + $this->assertEquals(0, $quiz->assignedUsers()->count()); + } + + public function testUnassignedUsersAreSkipped(): void + { + User::factory()->count(10)->create(); + $quiz = Quiz::factory()->create(); + + $this->action->execute($quiz, User::all()->pluck("id")); + $this->assertEquals(0, $quiz->assignedUsers()->count()); + + $this->action->execute($quiz, User::all()->pluck("id")); + $this->assertEquals(0, $quiz->assignedUsers()->count()); + } + + public function testUnknownUsersAreSkipped(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + $quiz->assignedUsers()->attach($user); + + $this->assertEquals(1, $quiz->assignedUsers()->count()); + $this->action->execute($quiz, collect([$user->id, 2, 3, 4, 5])); + $this->assertEquals(0, $quiz->assignedUsers()->count()); + } +} diff --git a/tests/Unit/UnlockQuizActionTest.php b/tests/Unit/UnlockQuizActionTest.php new file mode 100644 index 00000000..878dcb3b --- /dev/null +++ b/tests/Unit/UnlockQuizActionTest.php @@ -0,0 +1,35 @@ +action = new UnlockQuizAction(); + } + + public function testActionIsLockingQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(); + $this->action->execute($quiz); + + $this->assertDatabaseHas("quizzes", [ + "id" => $quiz->id, + "locked_at" => null, + ]); + } +} diff --git a/tests/Unit/UnpublishQuizRankingActionTest.php b/tests/Unit/UnpublishQuizRankingActionTest.php new file mode 100644 index 00000000..bb371d25 --- /dev/null +++ b/tests/Unit/UnpublishQuizRankingActionTest.php @@ -0,0 +1,35 @@ +action = new UnpublishQuizRankingAction(); + } + + public function testActionIsLockingQuiz(): void + { + $quiz = Quiz::factory()->withRanking()->create(); + $this->action->execute($quiz); + + $this->assertDatabaseHas("quizzes", [ + "id" => $quiz->id, + "ranking_published_at" => null, + ]); + } +} From 6aa38f6934f0ab08d0633ac876a7c2678d4044a0 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 19:48:16 +0100 Subject: [PATCH 06/23] test user model --- app/Models/Quiz.php | 6 +- tests/Unit/QuizModelTest.php | 153 +++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/QuizModelTest.php diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 22707ada..b32fcdfd 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -95,12 +95,12 @@ public function isRankingPublished(): Attribute public function canBeUnlocked(): Attribute { - return Attribute::get(fn(): bool => $this->isLocked && $this->scheduled_at > Carbon::now()); + return Attribute::get(fn(): bool => $this->isLocked && $this->scheduled_at->isFuture()); } public function canBeLocked(): Attribute { - return Attribute::get(fn(): bool => !$this->isLocked && $this->isReadyToBePublished() && $this->scheduled_at > Carbon::now()); + return Attribute::get(fn(): bool => !$this->isLocked && $this->isReadyToBePublished() && $this->scheduled_at->isFuture()); } public function closeAt(): Attribute @@ -110,7 +110,7 @@ public function closeAt(): Attribute public function isReadyToBePublished(): bool { - return $this->scheduled_at !== null && $this->duration !== null && $this->allQuestionsHaveCorrectAnswer(); + return $this->scheduled_at !== null && $this->duration !== null && $this->questions->count() > 0 && $this->allQuestionsHaveCorrectAnswer(); } public function hasUserQuizzesFrom(User $user): bool diff --git a/tests/Unit/QuizModelTest.php b/tests/Unit/QuizModelTest.php new file mode 100644 index 00000000..d219a9f4 --- /dev/null +++ b/tests/Unit/QuizModelTest.php @@ -0,0 +1,153 @@ +create(["locked_at" => Carbon::now()]); + $quiz2 = Quiz::factory()->create(["locked_at" => null]); + + $this->assertTrue($quiz1->isLocked); + $this->assertFalse($quiz2->isLocked); + } + + public function testIsPublished(): void + { + $quiz1 = Quiz::factory()->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->subHour()]); + $quiz2 = Quiz::factory()->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->addHour()]); + $quiz3 = Quiz::factory()->create(["locked_at" => null, "scheduled_at" => Carbon::now()->subHour()]); + + $this->assertTrue($quiz1->isPublished); + $this->assertFalse($quiz2->isPublished); + $this->assertFalse($quiz3->isPublished); + } + + public function testState(): void + { + $published = Quiz::factory()->create(["locked_at" => Carbon::now(), "duration" => 30, "scheduled_at" => Carbon::now()->subHour()]); + $locked = Quiz::factory()->create(["locked_at" => Carbon::now(), "duration" => 30, "scheduled_at" => Carbon::now()->addHour()]); + $unlocked = Quiz::factory()->create(["locked_at" => null, "scheduled_at" => null]); + + $this->assertEquals("published", $published->state); + $this->assertEquals("locked", $locked->state); + $this->assertEquals("unlocked", $unlocked->state); + } + + public function testIsUserAssigned(): void + { + $user = User::factory()->create(); + $quiz1 = Quiz::factory()->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->addHour()]); + $quiz2 = Quiz::factory()->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->addHour()]); + $quiz1->assignedUsers()->attach($user); + + $this->assertTrue($quiz1->isUserAssigned($user)); + $this->assertFalse($quiz2->isUserAssigned($user)); + } + + public function testIsRankingPublished(): void + { + $quiz1 = Quiz::factory()->create(["ranking_published_at" => Carbon::now()]); + $quiz2 = Quiz::factory()->create(["ranking_published_at" => null]); + + $this->assertTrue($quiz1->isRankingPublished); + $this->assertFalse($quiz2->isRankingPublished); + } + + public function testCanBeUnlocked(): void + { + $quiz1 = Quiz::factory()->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->addHour()]); + $quiz2 = Quiz::factory()->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->subHour()]); + $quiz3 = Quiz::factory()->create(["locked_at" => null, "scheduled_at" => null]); + + $this->assertTrue($quiz1->canBeUnlocked); + $this->assertFalse($quiz2->canBeUnlocked); + $this->assertFalse($quiz3->canBeUnlocked); + } + + public function testCanBeLocked(): void + { + $quiz1 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => null, "scheduled_at" => Carbon::now()->addHour(), "duration" => 30]); + $quiz2 = Quiz::factory()->create(["locked_at" => null, "scheduled_at" => Carbon::now()->addHour(), "duration" => 30]); + $quiz3 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => Carbon::now(), "scheduled_at" => Carbon::now()->addHour(), "duration" => 30]); + $quiz4 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => null, "scheduled_at" => Carbon::now()->subHour(), "duration" => 30]); + $quiz5 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => null, "scheduled_at" => Carbon::now()->addHour(), "duration" => null]); + + $this->assertTrue($quiz1->canBeLocked); + $this->assertFalse($quiz2->canBeLocked); + $this->assertFalse($quiz3->canBeLocked); + $this->assertFalse($quiz4->canBeLocked); + $this->assertFalse($quiz5->canBeLocked); + } + + public function testCloseAt(): void + { + $quiz1 = Quiz::factory()->has(Question::factory()->locked())->create(["scheduled_at" => Carbon::now(), "duration" => 30]); + $quiz2 = Quiz::factory()->create(["scheduled_at" => null]); + + $this->assertEquals("2024-10-11:08:30", $quiz1->closeAt->format("Y-m-d:H:i")); + $this->assertNull($quiz2->closeAt); + } + + public function testIsReadyToBePublished(): void + { + $quiz1 = Quiz::factory()->has(Question::factory()->locked())->create(["scheduled_at" => Carbon::now()->addHour(), "duration" => 30]); + $quiz2 = Quiz::factory()->create(["scheduled_at" => Carbon::now()->addHour(), "duration" => 30]); + $quiz3 = Quiz::factory()->has(Question::factory())->create(["scheduled_at" => Carbon::now()->addHour(), "duration" => 30]); + $quiz4 = Quiz::factory()->has(Question::factory()->locked())->create(["scheduled_at" => Carbon::now()->addHour(), "duration" => null]); + + $this->assertTrue($quiz1->isReadyToBePublished()); + $this->assertFalse($quiz2->isReadyToBePublished()); + $this->assertFalse($quiz3->isReadyToBePublished()); + $this->assertFalse($quiz4->isReadyToBePublished()); + } + + public function testHasUserQuizzesFrom(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $userQuiz = UserQuiz::factory(["user_id" => $user1])->create(); + + $this->assertTrue($userQuiz->quiz->hasUserQuizzesFrom($user1)); + $this->assertFalse($userQuiz->quiz->hasUserQuizzesFrom($user2)); + } + + public function testIsClosingToday(): void + { + $quiz1 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => Carbon::now(), "duration" => 30, "scheduled_at" => Carbon::now()]); + $quiz2 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => Carbon::now(), "duration" => 30, "scheduled_at" => Carbon::now()->addDay()]); + $quiz3 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => Carbon::now(), "duration" => 30, "scheduled_at" => Carbon::now()->subHour()]); + $quiz4 = Quiz::factory()->has(Question::factory()->locked())->create(["locked_at" => null, "duration" => 30, "scheduled_at" => Carbon::now()]); + + $this->assertTrue($quiz1->isClosingToday()); + $this->assertFalse($quiz2->isClosingToday()); + $this->assertFalse($quiz3->isClosingToday()); + $this->assertFalse($quiz4->isClosingToday()); + } +} From e10768fd19c74b7b3427270db28fa9bc0793eb41 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 22:36:24 +0100 Subject: [PATCH 07/23] remove randomness from test --- tests/Feature/QuizInviteTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/QuizInviteTest.php b/tests/Feature/QuizInviteTest.php index 47fac5c4..da8fadbb 100644 --- a/tests/Feature/QuizInviteTest.php +++ b/tests/Feature/QuizInviteTest.php @@ -63,7 +63,7 @@ public function testAdminCannotViewInvitePanelInPublishedQuiz(): void public function testFilteringAndSortingUsers(): void { $school = School::factory()->create(); - User::factory()->count(8)->create(); + User::factory()->count(8)->create(["firstname" => "test"]); $user = User::factory()->create([ "firstname" => "Jan", "school_id" => $school->id, From 364a5a684f27eb9f24c72420aa4911dcec7a6df3 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Tue, 10 Dec 2024 22:36:46 +0100 Subject: [PATCH 08/23] fix command name --- routes/console.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/routes/console.php b/routes/console.php index aafe430b..325e8e02 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,9 +2,6 @@ declare(strict_types=1); -use Illuminate\Foundation\Inspiring; -use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; -Artisan::command("inspire", function (): void { - $this->comment(Inspiring::quote()); -})->purpose("Display an inspiring quote")->hourly(); +Schedule::command('app:dispatch-user-quiz-closed-event')->daily(); From c854524c868a6e5dffc883b6803e7c4ed11aff1e Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 00:06:34 +0100 Subject: [PATCH 09/23] implement sending notification when quiz is closed --- app/Actions/LockQuizAction.php | 5 + .../Commands/DispatchUserQuizClosedEvent.php | 24 +++++ app/Events/AssignedQuizClosed.php | 23 +++++ app/Jobs/CloseUserQuizJob.php | 12 +++ .../SendAssignedQuizClosedNotification.php | 21 ++++ app/Listeners/SendQuizClosedNotification.php | 7 +- ...rQuizClosedMail.php => QuizClosedMail.php} | 14 +-- database/seeders/UserQuizSeeder.php | 4 +- resources/views/emails/quiz-closed.blade.php | 3 +- routes/console.php | 2 +- .../DispatchUserQuizClosedEventTest.php | 99 +++++++++++++++++++ tests/Feature/QuizTest.php | 4 +- tests/Unit/CloseUserQuizJobTest.php | 84 ++++++++++++++++ tests/Unit/LockQuizActionTest.php | 32 +++++- 14 files changed, 316 insertions(+), 18 deletions(-) create mode 100644 app/Console/Commands/DispatchUserQuizClosedEvent.php create mode 100644 app/Events/AssignedQuizClosed.php create mode 100644 app/Listeners/SendAssignedQuizClosedNotification.php rename app/Mail/{UserQuizClosedMail.php => QuizClosedMail.php} (67%) create mode 100644 tests/Feature/Commands/DispatchUserQuizClosedEventTest.php create mode 100644 tests/Unit/CloseUserQuizJobTest.php diff --git a/app/Actions/LockQuizAction.php b/app/Actions/LockQuizAction.php index a9392c03..aaa3a64c 100644 --- a/app/Actions/LockQuizAction.php +++ b/app/Actions/LockQuizAction.php @@ -4,6 +4,7 @@ namespace App\Actions; +use App\Jobs\CloseUserQuizJob; use App\Models\Quiz; use Carbon\Carbon; @@ -13,5 +14,9 @@ public function execute(Quiz $quiz): void { $quiz->locked_at = Carbon::now(); $quiz->save(); + + if ($quiz->isClosingToday()) { + CloseUserQuizJob::dispatch($quiz)->delay($quiz->closeAt); + } } } diff --git a/app/Console/Commands/DispatchUserQuizClosedEvent.php b/app/Console/Commands/DispatchUserQuizClosedEvent.php new file mode 100644 index 00000000..affcd912 --- /dev/null +++ b/app/Console/Commands/DispatchUserQuizClosedEvent.php @@ -0,0 +1,24 @@ +isClosingToday()) { + CloseUserQuizJob::dispatch($quiz)->delay($quiz->closeAt); + } + } + } +} diff --git a/app/Events/AssignedQuizClosed.php b/app/Events/AssignedQuizClosed.php new file mode 100644 index 00000000..f637ffea --- /dev/null +++ b/app/Events/AssignedQuizClosed.php @@ -0,0 +1,23 @@ +quiz->isPublished) { + return; + } + + $participants = $this->quiz->userQuizzes->pluck("user_id")->toArray(); + $absent = $this->quiz->assignedUsers()->whereNotIn("user_id", $participants)->get(); + + foreach ($absent as $user) { + event(new AssignedQuizClosed($this->quiz, $user)); + } + foreach ($this->quiz->userQuizzes as $userQuiz) { if (!$userQuiz->wasClosedManually()) { event(new UserQuizClosed($userQuiz)); diff --git a/app/Listeners/SendAssignedQuizClosedNotification.php b/app/Listeners/SendAssignedQuizClosedNotification.php new file mode 100644 index 00000000..580b7a75 --- /dev/null +++ b/app/Listeners/SendAssignedQuizClosedNotification.php @@ -0,0 +1,21 @@ +quiz; + $user = $event->user; + + Mail::to($user->email)->send(new QuizClosedMail($user, $quiz)); + } +} diff --git a/app/Listeners/SendQuizClosedNotification.php b/app/Listeners/SendQuizClosedNotification.php index 3d3d9bef..f7162fc3 100644 --- a/app/Listeners/SendQuizClosedNotification.php +++ b/app/Listeners/SendQuizClosedNotification.php @@ -5,7 +5,7 @@ namespace App\Listeners; use App\Events\UserQuizClosed; -use App\Mail\UserQuizClosedMail; +use App\Mail\QuizClosedMail; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Mail; @@ -13,8 +13,9 @@ class SendQuizClosedNotification implements ShouldQueue { public function handle(UserQuizClosed $event): void { - $quiz = $event->userQuiz; + $quiz = $event->userQuiz->quiz; + $user = $event->userQuiz->user; - Mail::to($quiz->user->email)->send(new UserQuizClosedMail($quiz)); + Mail::to($user->email)->send(new QuizClosedMail($user, $quiz)); } } diff --git a/app/Mail/UserQuizClosedMail.php b/app/Mail/QuizClosedMail.php similarity index 67% rename from app/Mail/UserQuizClosedMail.php rename to app/Mail/QuizClosedMail.php index 09693818..7fdcc67c 100644 --- a/app/Mail/UserQuizClosedMail.php +++ b/app/Mail/QuizClosedMail.php @@ -4,26 +4,28 @@ namespace App\Mail; -use App\Models\UserQuiz; +use App\Models\Quiz; +use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; -class UserQuizClosedMail extends Mailable +class QuizClosedMail extends Mailable { use Queueable; use SerializesModels; public function __construct( - private UserQuiz $userQuiz, + private User $user, + private Quiz $quiz, ) {} public function envelope(): Envelope { return new Envelope( - subject: $this->userQuiz->quiz->title . " — podsumowanie", + subject: $this->quiz->title . " — podsumowanie", ); } @@ -32,8 +34,8 @@ public function content(): Content return new Content( view: "emails.quiz-closed", with: [ - "user" => $this->userQuiz->user, - "userQuiz" => $this->userQuiz, + "user" => $this->user, + "quiz" => $this->quiz, ], ); } diff --git a/database/seeders/UserQuizSeeder.php b/database/seeders/UserQuizSeeder.php index a1e84b51..903bb79f 100644 --- a/database/seeders/UserQuizSeeder.php +++ b/database/seeders/UserQuizSeeder.php @@ -24,7 +24,7 @@ public function run(): void self::createUserQuizForUser($quiz, $user2, null); } - public static function createUserQuizForUser(Quiz $quiz, User $user, ?int $correctAnswersCount): void + public static function createUserQuizForUser(Quiz $quiz, User $user, ?int $correctAnswersCount): UserQuiz { $userQuiz = UserQuiz::factory() ->for($quiz) @@ -55,5 +55,7 @@ public static function createUserQuizForUser(Quiz $quiz, User $user, ?int $corre ->for($selectedAnswer) ->create(); } + + return $userQuiz; } } diff --git a/resources/views/emails/quiz-closed.blade.php b/resources/views/emails/quiz-closed.blade.php index 5a5569a1..51c59439 100644 --- a/resources/views/emails/quiz-closed.blade.php +++ b/resources/views/emails/quiz-closed.blade.php @@ -5,7 +5,6 @@ a { text-decoration: none; } p { margin: 48px 0; line-height: 24px; } table { width: 100%; border-spacing: 0; } - .button { background-color: #E4007D; color: white; padding: 16px 80px; border-radius: 12px; }
@@ -17,7 +16,7 @@

Cześć, {{ $user->firstname }}!

- Otrzymaliśmy Twoje odpowiedzi na test {{ $userQuiz->quiz->title }}.
+ Otrzymaliśmy Twoje odpowiedzi na test {{ $quiz->title }}.
Twój test jest obecnie sprawdzany, wyślemy powiadomienie na Twoją skrzynkę pocztową, gdy wyniki będą dostępne.

diff --git a/routes/console.php b/routes/console.php index 325e8e02..9dc702fe 100644 --- a/routes/console.php +++ b/routes/console.php @@ -4,4 +4,4 @@ use Illuminate\Support\Facades\Schedule; -Schedule::command('app:dispatch-user-quiz-closed-event')->daily(); +Schedule::command("app:dispatch-user-quiz-closed-event")->daily(); diff --git a/tests/Feature/Commands/DispatchUserQuizClosedEventTest.php b/tests/Feature/Commands/DispatchUserQuizClosedEventTest.php new file mode 100644 index 00000000..29683a20 --- /dev/null +++ b/tests/Feature/Commands/DispatchUserQuizClosedEventTest.php @@ -0,0 +1,99 @@ +seed(PublishedQuizSeeder::class); + $this->quiz = Quiz::query()->firstOrFail(); + $this->quiz->duration = 30; + $this->quiz->save(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + + public function testDispatchCloseUserQuizJob(): void + { + Queue::fake(); + + $this->quiz->scheduled_at = Carbon::now()->addHour(); + $this->quiz->save(); + + $this->artisan("app:dispatch-user-quiz-closed-event")->assertSuccessful(); + + Queue::assertPushed(CloseUserQuizJob::class, fn($job) => $job->delay->eq($this->quiz->closeAt)); + } + + public function testUserQuizClosedEventIsDispatched(): void + { + Event::fake([UserQuizClosed::class, AssignedQuizClosed::class]); + + $this->quiz->scheduled_at = Carbon::now()->addHour(); + $this->quiz->save(); + + $this->quiz->assignedUsers()->attach(User::factory()->create()); + UserQuizSeeder::createUserQuizForUser($this->quiz, User::factory()->create(), 2); + + $this->artisan("app:dispatch-user-quiz-closed-event")->assertSuccessful(); + Carbon::setTestNow($this->quiz->closeAt->addHour()); + + Event::assertListening(UserQuizClosed::class, SendQuizClosedNotification::class); + Event::assertListening(AssignedQuizClosed::class, SendAssignedQuizClosedNotification::class); + } + + public function testSkipAlreadyClosedQuizzes(): void + { + Queue::fake(); + + $this->quiz->scheduled_at = Carbon::now()->subDays(2); + $this->quiz->save(); + $this->quiz->refresh(); + + $this->artisan("app:dispatch-user-quiz-closed-event")->assertSuccessful(); + + Queue::assertNothingPushed(); + } + + public function testSkipQuizzesThatWillNotBeClosedToday(): void + { + Queue::fake(); + + $this->quiz->scheduled_at = Carbon::now()->addDay(); + $this->quiz->save(); + $this->quiz->refresh(); + + $this->artisan("app:dispatch-user-quiz-closed-event")->assertSuccessful(); + + Queue::assertNothingPushed(); + } +} diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index 7462051e..54ae7b0a 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -380,7 +380,7 @@ public function testAdminCannotCopyQuizThatNotExisted(): void public function testAdminCanLockQuiz(): void { - $quiz = Quiz::factory()->locked()->create(["locked_at" => null]); + $quiz = Quiz::factory()->has(Question::factory()->locked())->create(["scheduled_at" => Carbon::now(), "duration" => 30, "locked_at" => null]); $this->actingAs($this->admin) ->from("/") @@ -473,7 +473,7 @@ public function testAdminCannotUnlockQuizWithPastScheduledTime(): void public function testUserCanStartQuiz(): void { - $quiz = Quiz::factory()->locked()->create(); + $quiz = Quiz::factory()->locked()->has(Question::factory()->locked())->create(); $response = $this->actingAs($this->user) ->from("/") diff --git a/tests/Unit/CloseUserQuizJobTest.php b/tests/Unit/CloseUserQuizJobTest.php new file mode 100644 index 00000000..149e05cb --- /dev/null +++ b/tests/Unit/CloseUserQuizJobTest.php @@ -0,0 +1,84 @@ +seed(PublishedQuizSeeder::class); + $this->quiz = Quiz::query()->firstOrFail(); + } + + public function testEmitUserQuizClosedEvent(): void + { + Event::fake([UserQuizClosed::class, AssignedQuizClosed::class]); + + UserQuizSeeder::createUserQuizForUser($this->quiz, User::factory()->create(), 2); + CloseUserQuizJob::dispatch($this->quiz); + + Event::assertListening(UserQuizClosed::class, SendQuizClosedNotification::class); + Event::assertNotDispatched(AssignedQuizClosed::class); + } + + public function testSkipNotLockedQuizzes(): void + { + Event::fake([UserQuizClosed::class, AssignedQuizClosed::class]); + + $this->quiz->locked_at = null; + $this->quiz->save(); + + CloseUserQuizJob::dispatch($this->quiz); + + Event::assertNotDispatched(UserQuizClosed::class); + Event::assertNotDispatched(AssignedQuizClosed::class); + } + + public function testSkipManuallyClosedQuizzes(): void + { + Event::fake([UserQuizClosed::class, AssignedQuizClosed::class]); + + $userQuiz = UserQuizSeeder::createUserQuizForUser($this->quiz, User::factory()->create(), 2); + $userQuiz->closed_at = Carbon::now(); + $userQuiz->save(); + + CloseUserQuizJob::dispatch($this->quiz); + + Event::assertNotDispatched(UserQuizClosed::class); + Event::assertNotDispatched(AssignedQuizClosed::class); + } + + public function testEmitAssignedQuizClosedEventToUsersThatWereAssignedButDidNotParticipateInQuiz(): void + { + Event::fake([UserQuizClosed::class, AssignedQuizClosed::class]); + + $this->quiz->assignedUsers()->attach(User::factory()->create()); + + CloseUserQuizJob::dispatch($this->quiz); + + Event::assertNotDispatched(UserQuizClosed::class); + Event::assertListening(AssignedQuizClosed::class, SendAssignedQuizClosedNotification::class); + } +} diff --git a/tests/Unit/LockQuizActionTest.php b/tests/Unit/LockQuizActionTest.php index 00f10f94..85b92ba5 100644 --- a/tests/Unit/LockQuizActionTest.php +++ b/tests/Unit/LockQuizActionTest.php @@ -5,9 +5,12 @@ namespace Tests\Unit; use App\Actions\LockQuizAction; +use App\Jobs\CloseUserQuizJob; +use App\Models\Question; use App\Models\Quiz; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Queue; use Tests\TestCase; class LockQuizActionTest extends TestCase @@ -15,6 +18,7 @@ class LockQuizActionTest extends TestCase use RefreshDatabase; private LockQuizAction $action; + private Quiz $quiz; protected function setUp(): void { @@ -22,6 +26,7 @@ protected function setUp(): void Carbon::setTestNow(Carbon::create(2024, 10, 11, 8)); $this->action = new LockQuizAction(); + $this->quiz = Quiz::factory()->has(Question::factory()->locked())->create(["scheduled_at" => Carbon::now(), "duration" => 30]); } protected function tearDown(): void @@ -32,12 +37,33 @@ protected function tearDown(): void public function testActionIsLockingQuiz(): void { - $quiz = Quiz::factory()->create(); - $this->action->execute($quiz); + $this->action->execute($this->quiz); $this->assertDatabaseHas("quizzes", [ - "id" => $quiz->id, + "id" => $this->quiz->id, "locked_at" => "2024-10-11:08:00", ]); } + + public function testDispatchCloseUserQuizJobIfTestWillBeClosedToday(): void + { + Queue::fake(); + + $this->action->execute($this->quiz); + $this->artisan("app:dispatch-user-quiz-closed-event")->assertSuccessful(); + + Queue::assertPushed(CloseUserQuizJob::class, fn($job) => $job->delay->eq($this->quiz->closeAt)); + } + + public function testNotDispatchCloseUserQuizJobIfTestWillNotBeClosedToday(): void + { + Queue::fake(); + + $this->quiz->scheduled_at = Carbon::tomorrow(); + + $this->action->execute($this->quiz); + $this->artisan("app:dispatch-user-quiz-closed-event")->assertSuccessful(); + + Queue::assertNothingPushed(); + } } From aedc2a322827df9fb6198bfe08e16293e90d49e2 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 01:08:43 +0100 Subject: [PATCH 10/23] fix wasClosedManually method --- app/Models/UserQuiz.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/UserQuiz.php b/app/Models/UserQuiz.php index b6f21a1e..25366ab0 100644 --- a/app/Models/UserQuiz.php +++ b/app/Models/UserQuiz.php @@ -52,7 +52,7 @@ public function isClosed(): Attribute public function wasClosedManually(): bool { - return $this->closed_at !== null; + return $this->closed_at->ne($this->quiz->closeAt); } public function points(): Attribute From 029c95e0b7e3c4a1617c6f600c206de0fe5311df Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 01:09:07 +0100 Subject: [PATCH 11/23] prepare quizzes for testing --- app/Actions/CreateUserQuizAction.php | 5 --- database/seeders/DatabaseSeeder.php | 33 ++++++------------- database/seeders/PublishedQuizSeeder.php | 11 ++++--- tests/Unit/CreateUserQuizActionTest.php | 42 ------------------------ 4 files changed, 16 insertions(+), 75 deletions(-) diff --git a/app/Actions/CreateUserQuizAction.php b/app/Actions/CreateUserQuizAction.php index 041f93ab..2df95a36 100644 --- a/app/Actions/CreateUserQuizAction.php +++ b/app/Actions/CreateUserQuizAction.php @@ -4,7 +4,6 @@ namespace App\Actions; -use App\Jobs\CloseUserQuizJob; use App\Models\Quiz; use App\Models\User; use App\Models\UserQuiz; @@ -27,10 +26,6 @@ public function execute(Quiz $quiz, User $user): UserQuiz $this->action->execute($question, $userQuiz); } - if ($quiz->isClosingToday()) { - CloseUserQuizJob::dispatch($quiz)->delay($quiz->closeAt); - } - return $userQuiz; } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e630fe40..26724287 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -7,7 +7,6 @@ use App\Models\Answer; use App\Models\Question; use App\Models\Quiz; -use App\Models\User; use Carbon\Carbon; use Illuminate\Database\Seeder; @@ -20,30 +19,18 @@ public function run(): void AdminSeeder::class, ]); - Quiz::factory()->count(2)->create(["scheduled_at" => Carbon::now()->addMonth()]); - Quiz::factory()->locked()->count(2)->create(["scheduled_at" => Carbon::now()->subMonth(), "duration" => 6]); + Quiz::factory() + ->has(Question::factory()->count(5)->has(Answer::factory()->count(4))) + ->count(3) + ->create(["locked_at" => null, "duration" => 1, "scheduled_at" => Carbon::now()->addHour()]); - $quizzes = Quiz::factory()->count(4)->locked()->create(["scheduled_at" => Carbon::now()->addDay()]); + Quiz::factory() + ->has(Question::factory()->count(5)->has(Answer::factory()->count(4))) + ->count(3) + ->create(["locked_at" => null, "duration" => 1, "scheduled_at" => Carbon::now()->addMinutes(2)]); - foreach ($quizzes as $quiz) { - $questions = Question::factory()->count(4)->create(["quiz_id" => $quiz->id]); - - foreach ($questions as $question) { - $answers = Answer::factory()->count(4)->create(["question_id" => $question->id]); - $question->correct_answer_id = $answers[rand(0, 3)]->id; - $question->save(); - } + foreach (Quiz::all() as $quiz) { + PublishedQuizSeeder::selectRandomCorrectAnswer($quiz); } - - foreach (User::query()->role("user")->get() as $user) { - $userQuiz = $quiz->createUserQuiz($user); - - foreach ($userQuiz->userQuestions as $answer) { - $answer->answer()->associate($answer->question->answers->random()); - $answer->save(); - } - } - - User::factory()->count(10)->create(); } } diff --git a/database/seeders/PublishedQuizSeeder.php b/database/seeders/PublishedQuizSeeder.php index e933be11..7ac816c5 100644 --- a/database/seeders/PublishedQuizSeeder.php +++ b/database/seeders/PublishedQuizSeeder.php @@ -14,14 +14,15 @@ class PublishedQuizSeeder extends Seeder public function run(): void { $quiz = Quiz::factory() - ->has( - Question::factory() - ->count(5) - ->has(Answer::factory()->count(4)), - ) + ->has(Question::factory()->count(5)->has(Answer::factory()->count(4))) ->published() ->create(); + self::selectRandomCorrectAnswer($quiz); + } + + public static function selectRandomCorrectAnswer(Quiz $quiz): void + { foreach ($quiz->questions as $question) { $answers = $question->answers; diff --git a/tests/Unit/CreateUserQuizActionTest.php b/tests/Unit/CreateUserQuizActionTest.php index cedf7561..fb4b6a08 100644 --- a/tests/Unit/CreateUserQuizActionTest.php +++ b/tests/Unit/CreateUserQuizActionTest.php @@ -6,13 +6,10 @@ use App\Actions\CreateUserQuestionAction; use App\Actions\CreateUserQuizAction; -use App\Jobs\CloseUserQuizJob; use App\Models\Quiz; use App\Models\User; use Database\Seeders\PublishedQuizSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Queue; use Tests\TestCase; class CreateUserQuizActionTest extends TestCase @@ -27,21 +24,12 @@ protected function setUp(): void { parent::setUp(); - Queue::fake(); - Carbon::setTestNow(Carbon::create(2024, 10, 11, 8)); - $this->seed(PublishedQuizSeeder::class); $this->action = new CreateUserQuizAction(new CreateUserQuestionAction()); $this->quiz = Quiz::query()->firstOrFail(); $this->user = User::factory()->create(); } - protected function tearDown(): void - { - parent::tearDown(); - Carbon::setTestNow(); - } - public function testUserQuizIsCreated(): void { $userQuiz = $this->action->execute($this->quiz, $this->user); @@ -55,34 +43,4 @@ public function testUserQuizIsCreated(): void $this->assertDatabaseCount("user_questions", 5); } - - public function testCloseUserQuizJobIsDispatchedIfQuizIsClosingToday(): void - { - $this->quiz->scheduled_at = Carbon::now()->addHour(); - $this->quiz->save(); - - $this->action->execute($this->quiz, $this->user); - - Queue::assertPushed(CloseUserQuizJob::class); - } - - public function testCloseUserQuizJobIsNotDispatchedIfQuizIsAlreadyClosed(): void - { - $this->quiz->scheduled_at = Carbon::now()->subHours(3); - $this->quiz->save(); - - $this->action->execute($this->quiz, $this->user); - - Queue::assertNotPushed(CloseUserQuizJob::class); - } - - public function testCloseUserQuizJobIsNotDispatchedIfQuizIsNotClosingToday(): void - { - $this->quiz->scheduled_at = Carbon::now()->addDay(); - $this->quiz->save(); - - $this->action->execute($this->quiz, $this->user); - - Queue::assertNotPushed(CloseUserQuizJob::class); - } } From bad26cd1bb01959de5eeeac134631485460a103a Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 01:32:22 +0100 Subject: [PATCH 12/23] fix filterArchivedQuizzes method --- app/Http/Controllers/QuizController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index 3630ea64..d8575907 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -114,7 +114,7 @@ private function filterArchivedQuizzes(Builder $query, Request $request): Builde $showArchived = $request->query("archived", "false") === "true"; if (!$showArchived) { - return $query->orWhere(fn(Builder $query) => $query->whereNull("locked_at")->orWhereDate("scheduled_at", ">", Carbon::now())); + return $query->orWhere(fn(Builder $query) => $query->whereNull("locked_at")->orWhere("scheduled_at", ">", Carbon::now())); } return $query; From 3a18bfa12b3e1844bebf532987647031ac088f43 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 06:36:36 +0100 Subject: [PATCH 13/23] add archived quiz to seeder --- database/seeders/DatabaseSeeder.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 26724287..027997b7 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -7,6 +7,9 @@ use App\Models\Answer; use App\Models\Question; use App\Models\Quiz; +use App\Models\School; +use App\Models\User; +use App\Models\UserQuiz; use Carbon\Carbon; use Illuminate\Database\Seeder; @@ -19,6 +22,10 @@ public function run(): void AdminSeeder::class, ]); + Quiz::factory() + ->has(Question::factory()->has(Answer::factory()->count(4))) + ->create(["locked_at" => Carbon::now()->subHour(), "duration" => 60, "scheduled_at" => Carbon::now()]); + Quiz::factory() ->has(Question::factory()->count(5)->has(Answer::factory()->count(4))) ->count(3) @@ -29,8 +36,20 @@ public function run(): void ->count(3) ->create(["locked_at" => null, "duration" => 1, "scheduled_at" => Carbon::now()->addMinutes(2)]); + $archivedQuiz = Quiz::factory() + ->has(Question::factory()->has(Answer::factory()->count(4))) + ->create(["locked_at" => Carbon::now()->subDays(2), "duration" => 60, "scheduled_at" => Carbon::now()->subDay()]); + foreach (Quiz::all() as $quiz) { PublishedQuizSeeder::selectRandomCorrectAnswer($quiz); } + + User::factory()->count(10)->has(School::factory())->create(); + User::factory()->count(10)->has(School::factory())->create(); + User::factory()->count(10)->has(School::factory())->create(); + + foreach (User::query()->role("user")->get() as $user) { + UserQuizSeeder::createUserQuizForUser($archivedQuiz, $user, null); + } } } From 0cc5238d368637496732ab322861d5af2c904c36 Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 06:36:49 +0100 Subject: [PATCH 14/23] fix code style --- database/seeders/DatabaseSeeder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 027997b7..5c54064f 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -9,7 +9,6 @@ use App\Models\Quiz; use App\Models\School; use App\Models\User; -use App\Models\UserQuiz; use Carbon\Carbon; use Illuminate\Database\Seeder; From 95ffc26bf2378fd8ae0f7618025b6fe483a7c55b Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Wed, 11 Dec 2024 06:53:44 +0100 Subject: [PATCH 15/23] fix no answer warning --- resources/js/components/UserQuiz/QuizPage.vue | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/resources/js/components/UserQuiz/QuizPage.vue b/resources/js/components/UserQuiz/QuizPage.vue index 63daff6e..d372849f 100644 --- a/resources/js/components/UserQuiz/QuizPage.vue +++ b/resources/js/components/UserQuiz/QuizPage.vue @@ -18,7 +18,7 @@ const questions = ref(props.userQuiz.questions) const emit = defineEmits<{ answer: [question: UserQuestion, selectedAnswer: number] }>() const allQuestionsAnswered = computed( - () => questions.value.every(question => question.selectedAnswer !== undefined), + () => questions.value.every(question => !!question.selectedAnswer ), ) const timeout = ref(false) @@ -52,7 +52,7 @@ function handleAnswer(question: UserQuestion, selectedAnswer: number) { -

To już wszystkie pytania. Czy chcesz oddać test?

- + Oddaj test - +