diff --git a/app/Helpers/DateFormatHelper.php b/app/Helpers/DateFormatHelper.php new file mode 100644 index 00000000..c1fc1e23 --- /dev/null +++ b/app/Helpers/DateFormatHelper.php @@ -0,0 +1,10 @@ +with("success", "Quiz updated"); } - public function lock(Quiz $quiz): RedirectResponse - { - $quiz->locked_at = Carbon::now(); - $quiz->save(); - - return redirect() - ->back() - ->with("success", "Quiz locked"); - } - public function destroy(Quiz $quiz): RedirectResponse { $quiz->delete(); @@ -77,4 +67,12 @@ public function clone(Quiz $quiz): RedirectResponse ->back() ->with("success", "Quiz cloned"); } + + public function createSubmission(Request $request, Quiz $quiz): RedirectResponse + { + $user = $request->user(); + $submission = $quiz->createSubmission($user); + + return redirect("/submissions/{$submission->id}/"); + } } diff --git a/app/Http/Controllers/QuizSubmissionController.php b/app/Http/Controllers/QuizSubmissionController.php new file mode 100644 index 00000000..d36d2263 --- /dev/null +++ b/app/Http/Controllers/QuizSubmissionController.php @@ -0,0 +1,20 @@ +load(["answerRecords.question.answers", "quiz"]); + + return Inertia::render("Submission/Show", ["submission" => QuizSubmissionResource::make($quizSubmission)]); + } +} diff --git a/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php b/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php new file mode 100644 index 00000000..19924b42 --- /dev/null +++ b/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php @@ -0,0 +1,28 @@ +user(); + $quiz = $request->route()->parameter("quiz"); + + $submission = QuizSubmission::query()->where(["quiz_id" => $quiz->id, "user_id" => $user->id])->first(); + + if ($submission) { + return redirect(route("submissions.show", $submission->id)); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/QuizRequest.php b/app/Http/Requests/QuizRequest.php index ae942518..14c4a036 100644 --- a/app/Http/Requests/QuizRequest.php +++ b/app/Http/Requests/QuizRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests; +use App\Helpers\DateFormatHelper; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; @@ -21,6 +22,8 @@ public function rules(): array { return [ "name" => ["required", "string"], + "scheduled_at" => ["date", "date_format:" . DateFormatHelper::DATETIME_FORMAT, "after:now"], + "duration" => ["integer", "min:1"], ]; } } diff --git a/app/Http/Resources/AnswerRecordResource.php b/app/Http/Resources/AnswerRecordResource.php new file mode 100644 index 00000000..8134b435 --- /dev/null +++ b/app/Http/Resources/AnswerRecordResource.php @@ -0,0 +1,33 @@ +question->answers as $answer) { + $answers->add([ + "id" => $answer->id, + "text" => $answer->text, + ]); + } + + return [ + "id" => $this->id, + "question" => $this->question->text, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "closed" => $this->isClosed, + "answers" => $answers->shuffle(), + "selected" => $this->answer_id, + ]; + } +} diff --git a/app/Http/Resources/QuizResource.php b/app/Http/Resources/QuizResource.php index 130fdcf1..e60d5053 100644 --- a/app/Http/Resources/QuizResource.php +++ b/app/Http/Resources/QuizResource.php @@ -15,6 +15,7 @@ public function toArray($request): array "name" => $this->name, "createdAt" => $this->created_at, "updatedAt" => $this->updated_at, + "duration" => $this->duration, "locked" => $this->isLocked, "questions" => QuestionResource::collection($this->questions), ]; diff --git a/app/Http/Resources/QuizSubmissionResource.php b/app/Http/Resources/QuizSubmissionResource.php new file mode 100644 index 00000000..18cbcedc --- /dev/null +++ b/app/Http/Resources/QuizSubmissionResource.php @@ -0,0 +1,24 @@ + $this->id, + "name" => $this->quiz->name, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "closedAt" => $this->closed_at, + "closed" => $this->isClosed, + "answers" => AnswerRecordResource::collection($this->answerRecords->shuffle()), + ]; + } +} diff --git a/app/Models/AnswerRecord.php b/app/Models/AnswerRecord.php new file mode 100644 index 00000000..ec0f843e --- /dev/null +++ b/app/Models/AnswerRecord.php @@ -0,0 +1,48 @@ +belongsTo(QuizSubmission::class); + } + + public function answer(): BelongsTo + { + return $this->belongsTo(Answer::class); + } + + public function question(): BelongsTo + { + return $this->belongsTo(Question::class); + } + + public function isClosed(): Attribute + { + return Attribute::get(fn(): bool => $this->quizSubmission->isClosed); + } +} diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 16eb79d3..9b5f259e 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -17,8 +17,10 @@ * @property string $name * @property Carbon $created_at * @property Carbon $updated_at - * @property Carbon $locked_at + * @property Carbon $scheduled_at + * @property ?int $duration * @property bool $isLocked + * @property ?Carbon $closeAt * @property Collection $questions * @property Collection $answers */ @@ -28,6 +30,8 @@ class Quiz extends Model protected $fillable = [ "name", + "scheduled_at", + "duration", ]; public function questions(): HasMany @@ -42,7 +46,12 @@ public function answers(): HasManyThrough public function isLocked(): Attribute { - return Attribute::get(fn(): bool => $this->locked_at !== null); + return Attribute::get(fn(): bool => $this->isReadyToBePublished() && $this->scheduled_at <= Carbon::now()); + } + + public function closeAt(): Attribute + { + return Attribute::get(fn(): ?Carbon => $this->isReadyToBePublished() ? $this->scheduled_at->copy()->addMinutes($this->duration) : null); } public function clone(): self @@ -57,10 +66,33 @@ public function clone(): self return $quizCopy; } + public function createSubmission(User $user): QuizSubmission + { + $submission = new QuizSubmission(); + $submission->closed_at = $this->closeAt; + $submission->quiz()->associate($this); + $submission->user()->associate($user); + $submission->save(); + + foreach ($this->questions as $question) { + $answerRecord = new AnswerRecord(); + $answerRecord->quizSubmission()->associate($submission); + $answerRecord->question()->associate($question); + $answerRecord->save(); + } + + return $submission; + } + + protected function isReadyToBePublished(): bool + { + return $this->scheduled_at !== null && $this->duration !== null; + } + protected function casts(): array { return [ - "locked_at" => "datetime", + "scheduled_at" => "datetime", ]; } } diff --git a/app/Models/QuizSubmission.php b/app/Models/QuizSubmission.php new file mode 100644 index 00000000..cc4fdd33 --- /dev/null +++ b/app/Models/QuizSubmission.php @@ -0,0 +1,57 @@ + $answerRecords + */ +class QuizSubmission extends Model +{ + use HasFactory; + + public function quiz(): BelongsTo + { + return $this->belongsTo(Quiz::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function answerRecords(): HasMany + { + return $this->hasMany(AnswerRecord::class); + } + + public function isClosed(): Attribute + { + return Attribute::get(fn(): bool => $this->closed_at !== null); + } + + protected function casts(): array + { + return [ + "closed_at" => "datetime", + ]; + } +} diff --git a/app/Policies/QuizPolicy.php b/app/Policies/QuizPolicy.php index 510c4332..4b110bf1 100644 --- a/app/Policies/QuizPolicy.php +++ b/app/Policies/QuizPolicy.php @@ -18,4 +18,9 @@ public function delete(User $user, Quiz $quiz): bool { return !$quiz->isLocked; } + + public function submit(User $user, Quiz $quiz): bool + { + return $quiz->isLocked; + } } diff --git a/app/Policies/QuizSubmissionPolicy.php b/app/Policies/QuizSubmissionPolicy.php new file mode 100644 index 00000000..0fbe652d --- /dev/null +++ b/app/Policies/QuizSubmissionPolicy.php @@ -0,0 +1,16 @@ +id === $quizSubmission->user_id; + } +} diff --git a/database/factories/AnswerRecordFactory.php b/database/factories/AnswerRecordFactory.php new file mode 100644 index 00000000..d0ee29f6 --- /dev/null +++ b/database/factories/AnswerRecordFactory.php @@ -0,0 +1,24 @@ + + */ +class AnswerRecordFactory extends Factory +{ + public function definition(): array + { + return [ + "quiz_submission_id" => QuizSubmission::factory(), + "question_id" => Question::factory(), + ]; + } +} diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index be5b4a31..ea1a0582 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -17,13 +17,14 @@ public function definition(): array { return [ "name" => fake()->name(), + "duration" => fake()->numberBetween(60, 120), ]; } public function locked(): static { return $this->state(fn(array $attributes): array => [ - "locked_at" => Carbon::now(), + "scheduled_at" => Carbon::now(), ]); } } diff --git a/database/factories/QuizSubmissionFactory.php b/database/factories/QuizSubmissionFactory.php new file mode 100644 index 00000000..63f52d41 --- /dev/null +++ b/database/factories/QuizSubmissionFactory.php @@ -0,0 +1,32 @@ + + */ +class QuizSubmissionFactory extends Factory +{ + public function definition(): array + { + return [ + "quiz_id" => Quiz::factory()->locked(), + "user_id" => User::factory(), + ]; + } + + public function closed(): static + { + return $this->state(fn(array $attributes): array => [ + "closed_at" => Carbon::now(), + ]); + } +} diff --git a/database/migrations/2024_08_13_104625_create_quiz_submissions_table.php b/database/migrations/2024_08_13_104625_create_quiz_submissions_table.php new file mode 100644 index 00000000..59639aa2 --- /dev/null +++ b/database/migrations/2024_08_13_104625_create_quiz_submissions_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignIdFor(Quiz::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); + $table->timestamp("closed_at")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("quiz_submissions"); + } +}; diff --git a/database/migrations/2024_08_13_104743_create_answer_records_table.php b/database/migrations/2024_08_13_104743_create_answer_records_table.php new file mode 100644 index 00000000..e026e30d --- /dev/null +++ b/database/migrations/2024_08_13_104743_create_answer_records_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignIdFor(QuizSubmission::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Answer::class)->nullable()->constrained()->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("answer_records"); + } +}; diff --git a/database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php b/database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php new file mode 100644 index 00000000..4a278e6b --- /dev/null +++ b/database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php @@ -0,0 +1,23 @@ +timestamp("scheduled_at")->nullable(); + }); + } + + public function down(): void + { + Schema::table("quizzes", function (Blueprint $table): void { + $table->dropColumn("scheduled_at"); + }); + } +}; diff --git a/database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php b/database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php new file mode 100644 index 00000000..783b929b --- /dev/null +++ b/database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php @@ -0,0 +1,23 @@ +dropColumn("locked_at"); + }); + } + + public function down(): void + { + Schema::table("quizzes", function (Blueprint $table): void { + $table->timestamp("locked_at")->nullable(); + }); + } +}; diff --git a/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php b/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php new file mode 100644 index 00000000..0bcb2002 --- /dev/null +++ b/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php @@ -0,0 +1,23 @@ +unsignedInteger("duration")->nullable(); + }); + } + + public function down(): void + { + Schema::table("quizzes", function (Blueprint $table): void { + $table->dropColumn("duration"); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cff3b9ec..7100a3df 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,15 +4,13 @@ namespace Database\Seeders; -use App\Models\Answer; -use App\Models\Quiz; +use App\Models\AnswerRecord; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { - Quiz::factory()->create(); - Answer::factory()->create(); + AnswerRecord::factory()->create(); } } diff --git a/resources/js/Pages/Submission/Show.vue b/resources/js/Pages/Submission/Show.vue new file mode 100644 index 00000000..e69de29b diff --git a/resources/js/Types/AnswerRecord.d.ts b/resources/js/Types/AnswerRecord.d.ts new file mode 100644 index 00000000..c01a2b4b --- /dev/null +++ b/resources/js/Types/AnswerRecord.d.ts @@ -0,0 +1,14 @@ +export interface AnswerRecordQuestion { + id: number + text: string +} + +export interface AnswerRecord { + id: number + question: string + createAt: string + updatedAt: string + closed: boolean + selected?: number + answers: AnswerRecordQuestion[] +} diff --git a/resources/js/Types/Quiz.d.ts b/resources/js/Types/Quiz.d.ts index 197c246e..5d3b430d 100644 --- a/resources/js/Types/Quiz.d.ts +++ b/resources/js/Types/Quiz.d.ts @@ -5,6 +5,7 @@ export interface Quiz { name: string createdAt: number updatedAt: number + duration?: number locked: boolean questions: Question[] } diff --git a/resources/js/Types/QuizSubmission.d.ts b/resources/js/Types/QuizSubmission.d.ts new file mode 100644 index 00000000..f57e5cfc --- /dev/null +++ b/resources/js/Types/QuizSubmission.d.ts @@ -0,0 +1,11 @@ +import {type AnswerRecord} from '@/Types/AnswerRecord' + +export interface QuizSubmission { + id: number + name: string + createAt: string + updatedAt: string + closedAt?: string + closed: boolean + answers: AnswerRecord[] +} diff --git a/routes/web.php b/routes/web.php index 41ee4ce3..57c2b9a3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,8 @@ use App\Http\Controllers\QuestionAnswerController; use App\Http\Controllers\QuizController; use App\Http\Controllers\QuizQuestionController; +use App\Http\Controllers\QuizSubmissionController; +use App\Http\Middleware\EnsureQuizIsNotAlreadyStarted; use App\Models\Answer; use App\Models\Question; use Illuminate\Support\Facades\Route; @@ -12,26 +14,30 @@ Route::get("/", fn(): Response => inertia("Home")); -Route::get("/quizzes", [QuizController::class, "index"]); -Route::post("/quizzes", [QuizController::class, "store"]); -Route::get("/quizzes/{quiz}", [QuizController::class, "show"]); -Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz"); -Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz"); -Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); -Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"]); +Route::group(["prefix" => "admin"], function (): void { + Route::get("/quizzes", [QuizController::class, "index"])->name("admin.quizzes.index"); + Route::post("/quizzes", [QuizController::class, "store"])->name("admin.quizzes.store"); + Route::get("/quizzes/{quiz}", [QuizController::class, "show"])->name("admin.quizzes.show"); + Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz")->name("admin.quizzes.update"); + Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz")->name("admin.quizzes.destroy"); + Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"])->name("admin.quizzes.clone"); -Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"]); -Route::post("/quizzes/{quiz}/questions", [QuizQuestionController::class, "store"])->can("create," . Question::class . ",quiz"); -Route::get("/questions/{question}", [QuizQuestionController::class, "show"]); -Route::patch("/questions/{question}", [QuizQuestionController::class, "update"])->can("update,question"); -Route::delete("/questions/{question}", [QuizQuestionController::class, "destroy"])->can("delete,question"); -Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"])->can("clone,question,quiz"); + Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"])->name("admin.questions.index"); + Route::post("/quizzes/{quiz}/questions", [QuizQuestionController::class, "store"])->can("create," . Question::class . ",quiz")->name("admin.questions.store"); + Route::get("/questions/{question}", [QuizQuestionController::class, "show"])->name("admin.questions.show"); + Route::patch("/questions/{question}", [QuizQuestionController::class, "update"])->can("update,question")->name("admin.questions.update"); + Route::delete("/questions/{question}", [QuizQuestionController::class, "destroy"])->can("delete,question")->name("admin.questions.destroy"); + Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"])->can("clone,question,quiz")->name("admin.questions.clone"); -Route::get("/questions/{question}/answers", [QuestionAnswerController::class, "index"]); -Route::post("/questions/{question}/answers", [QuestionAnswerController::class, "store"])->can("create," . Answer::class . ",question"); -Route::get("/answers/{answer}", [QuestionAnswerController::class, "show"]); -Route::patch("/answers/{answer}", [QuestionAnswerController::class, "update"])->can("update,answer"); -Route::delete("/answers/{answer}", [QuestionAnswerController::class, "destroy"])->can("delete,answer"); -Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"])->can("clone,answer,question"); -Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer"); -Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer"); + Route::get("/questions/{question}/answers", [QuestionAnswerController::class, "index"])->name("admin.answers.index"); + Route::post("/questions/{question}/answers", [QuestionAnswerController::class, "store"])->can("create," . Answer::class . ",question")->name("admin.answers.store"); + Route::get("/answers/{answer}", [QuestionAnswerController::class, "show"])->name("admin.answers.show"); + Route::patch("/answers/{answer}", [QuestionAnswerController::class, "update"])->can("update,answer")->name("admin.answers.update"); + Route::delete("/answers/{answer}", [QuestionAnswerController::class, "destroy"])->can("delete,answer")->name("admin.answers.destroy"); + Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"])->can("clone,answer,question")->name("admin.answers.clone"); + Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer")->name("admin.answers.correct"); + Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer")->name("admin.answers.invalid"); +}); + +Route::post("/quizzes/{quiz}/start", [QuizController::class, "createSubmission"])->middleware(EnsureQuizIsNotAlreadyStarted::class)->can("submit,quiz")->name("quizzes.start"); +Route::get("/submissions/{quizSubmission}/", [QuizSubmissionController::class, "show"])->can("view,quizSubmission")->name("submissions.show"); diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php index 6546052c..4a0b4f92 100644 --- a/tests/Feature/AnswerTest.php +++ b/tests/Feature/AnswerTest.php @@ -32,7 +32,7 @@ public function testUserCanViewQuestionAnswers(): void $this->assertDatabaseCount("answers", 10); $this->actingAs($this->user) - ->get("/questions/{$question->id}/answers") + ->get(route("admin.answers.index", $question->id)) ->assertInertia( fn(Assert $page) => $page ->component("Answer/Index") @@ -42,7 +42,7 @@ public function testUserCanViewQuestionAnswers(): void public function testUserCannotViewAnswersOfQuestionThatNotExisted(): void { - $this->actingAs($this->user)->get("/questions/1/answers") + $this->actingAs($this->user)->get(route("admin.answers.index", 1)) ->assertStatus(404); } @@ -53,7 +53,7 @@ public function testUserCanViewSingleAnswer(): void $this->assertDatabaseCount("answers", 1); $this->actingAs($this->user) - ->get("/answers/{$answer->id}") + ->get(route("admin.answers.show", $answer->id)) ->assertInertia( fn(Assert $page) => $page ->component("Answer/Show") @@ -68,7 +68,7 @@ public function testUserCanViewLockedAnswer(): void $this->assertDatabaseCount("answers", 1); $this->actingAs($this->user) - ->get("/answers/{$answer->id}") + ->get(route("admin.answers.show", $answer->id)) ->assertInertia( fn(Assert $page) => $page ->component("Answer/Show") @@ -79,7 +79,7 @@ public function testUserCanViewLockedAnswer(): void public function testUserCannotViewAnswerThatNotExisted(): void { - $this->actingAs($this->user)->get("/answers/1") + $this->actingAs($this->user)->get(route("admin.answers.show", 1)) ->assertStatus(404); } @@ -89,7 +89,7 @@ public function testUserCanCreateAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", [ @@ -104,15 +104,15 @@ public function testUserCanCreateMultipleAnswers(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 1"]) ->assertRedirect("/quizzes"); $this->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 2"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 2"]) ->assertRedirect("/quizzes"); $this->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 3"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 3"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["text" => "Example answer 1"]); @@ -124,7 +124,7 @@ public function testUserCannotCreateAnswerToQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/1/answers", ["text" => "Example answer"]) + ->post(route("admin.answers.store", 1), ["text" => "Example answer"]) ->assertStatus(404); $this->assertDatabaseMissing("answers", [ @@ -138,7 +138,7 @@ public function testUserCannotCreateAnswerToQuestionThatIsLocked(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 1"]) ->assertStatus(403); $this->assertDatabaseMissing("answers", [ @@ -152,11 +152,11 @@ public function testUserCannotCreateInvalidAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", []) + ->post(route("admin.answers.store", $question->id), []) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => false]) + ->post(route("admin.answers.store", $question->id), ["text" => false]) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("answers", 0); @@ -168,7 +168,7 @@ public function testUserCanEditAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->patch(route("admin.answers.update", $answer->id), ["text" => "New answer"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["text" => "New answer"]); @@ -178,7 +178,7 @@ public function testUserCannotEditAnswerThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/1", ["text" => "New answer"]) + ->patch(route("admin.answers.update", 1), ["text" => "New answer"]) ->assertStatus(404); } @@ -188,11 +188,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/{$answer->id}", []) + ->patch(route("admin.answers.update", $answer->id), []) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") - ->patch("/answers/{$answer->id}", ["text" => true]) + ->patch(route("admin.answers.update", $answer->id), ["text" => true]) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); @@ -204,7 +204,7 @@ public function testUserCannotEditLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->patch(route("admin.answers.update", $answer->id), ["text" => "New answer"]) ->assertStatus(403); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); @@ -216,7 +216,7 @@ public function testUserCanDeleteAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/{$answer->id}") + ->delete(route("admin.answers.destroy", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseMissing("answers", ["text" => "answer"]); @@ -228,7 +228,7 @@ public function testUserCannotDeleteLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/{$answer->id}") + ->delete(route("admin.answers.destroy", $answer->id)) ->assertStatus(403); $this->assertDatabaseHas("answers", ["text" => "answer"]); @@ -238,7 +238,7 @@ public function testUserCannotDeleteAnswerThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/1") + ->delete(route("admin.answers.destroy", 1)) ->assertStatus(404); } @@ -248,7 +248,7 @@ public function testUserCanMarkAnswerAsCorrect(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/correct") + ->post(route("admin.answers.correct", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -267,7 +267,7 @@ public function testUserCanChangeCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answerB->id}/correct") + ->post(route("admin.answers.correct", $answerB->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerB->id]); @@ -285,7 +285,7 @@ public function testUserCanDeleteCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/{$answer->id}") + ->delete(route("admin.answers.destroy", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => null]); @@ -304,7 +304,7 @@ public function testUserCannotChangeCorrectAnswerInLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answerB->id}/correct") + ->post(route("admin.answers.correct", $answerA->id)) ->assertStatus(403); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); @@ -322,7 +322,7 @@ public function testUserCanChangeCorrectAnswerToInvalid(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/invalid") + ->post(route("admin.answers.invalid", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -340,7 +340,7 @@ public function testUserCannotChangeCorrectAnswerToInvalidInLockedQuestion(): vo $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/invalid") + ->post(route("admin.answers.invalid", $answer->id)) ->assertStatus(403); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -356,7 +356,7 @@ public function testUserCanCopyAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -372,7 +372,7 @@ public function testUserCanCopyLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -388,7 +388,7 @@ public function testUserCannotCopyAnswerToLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertStatus(403); $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); @@ -408,7 +408,7 @@ public function testUserCanCopyCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -422,7 +422,7 @@ public function testUserCannotCopyAnswerThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/2/clone/{$question->id}") + ->post(route("admin.answers.clone", ["answer" => 2, "question" => $question->id])) ->assertStatus(404); } @@ -432,7 +432,7 @@ public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/2") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => 2])) ->assertStatus(404); } } diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 673235a1..3eb614b3 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -36,7 +36,7 @@ public function testUserCanViewQuizQuestions(): void $this->assertDatabaseCount("answers", 10); $this->actingAs($this->user) - ->get("/quizzes/{$quiz->id}/questions") + ->get(route("admin.questions.index", $quiz->id)) ->assertInertia( fn(Assert $page) => $page ->component("Question/Index") @@ -47,7 +47,7 @@ public function testUserCanViewQuizQuestions(): void public function testUserCannotViewQuestionsOfQuizThatNotExisted(): void { - $this->actingAs($this->user)->get("/quizzes/1/questions") + $this->actingAs($this->user)->get(route("admin.questions.index", 1)) ->assertStatus(404); } @@ -58,7 +58,7 @@ public function testUserCanViewSingleQuestion(): void $this->assertDatabaseCount("questions", 1); $this->actingAs($this->user) - ->get("/questions/{$question->id}") + ->get(route("admin.questions.show", $question->id)) ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") @@ -73,7 +73,7 @@ public function testUserCanViewLockedQuestion(): void $this->assertDatabaseCount("questions", 1); $this->actingAs($this->user) - ->get("/questions/{$question->id}") + ->get(route("admin.questions.show", $question->id)) ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") @@ -84,7 +84,7 @@ public function testUserCanViewLockedQuestion(): void public function testUserCannotViewQuestionThatNotExisted(): void { - $this->actingAs($this->user)->get("/questions/1") + $this->actingAs($this->user)->get(route("admin.questions.show", 1)) ->assertStatus(404); } @@ -94,7 +94,7 @@ public function testUserCanCreateQuestion(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", [ @@ -109,15 +109,15 @@ public function testUserCanCreateMultipleQuestions(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 1"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 2"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 3"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", ["text" => "Example question 1"]); @@ -129,7 +129,7 @@ public function testUserCannotCreateQuestionToQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes/1/questions", ["text" => "Example question"]) + ->post(route("admin.questions.store", 1), ["text" => "Example question"]) ->assertStatus(404); $this->assertDatabaseMissing("questions", [ @@ -143,7 +143,7 @@ public function testUserCannotCreateQuestionToQuizThatIsLocked(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 1"]) ->assertStatus(403); $this->assertDatabaseMissing("questions", [ @@ -157,11 +157,11 @@ public function testUserCannotCreateInvalidQuestion(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/questions", []) + ->post(route("admin.questions.store", $quiz->id), []) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => false]) + ->post(route("admin.questions.store", $quiz->id), ["text" => false]) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("questions", 0); @@ -173,7 +173,7 @@ public function testUserCanEditQuestion(): void $this->actingAs($this->user) ->from("/") - ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->patch(route("admin.questions.update", $question->id), ["text" => "New question"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", ["text" => "New question"]); @@ -183,7 +183,7 @@ public function testUserCannotEditQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->patch("/questions/1", ["text" => "New question"]) + ->patch(route("admin.questions.update", 1), ["text" => "New question"]) ->assertStatus(404); } @@ -193,11 +193,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/") - ->patch("/questions/{$question->id}", []) + ->patch(route("admin.questions.update", $question->id), []) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") - ->patch("/questions/{$question->id}", ["text" => true]) + ->patch(route("admin.questions.update", $question->id), ["text" => true]) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("questions", ["text" => "Old questions"]); @@ -209,7 +209,7 @@ public function testUserCannotEditLockedQuestion(): void $this->actingAs($this->user) ->from("/") - ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->patch(route("admin.questions.update", $question->id), ["text" => "New question"]) ->assertStatus(403); $this->assertDatabaseHas("questions", ["text" => "Old question"]); @@ -226,7 +226,7 @@ public function testUserCanDeleteQuestion(): void $this->actingAs($this->user) ->from("/") - ->delete("/questions/{$question->id}") + ->delete(route("admin.questions.destroy", $question->id)) ->assertRedirect("/"); $this->assertDatabaseMissing("questions", ["text" => "question"]); @@ -241,7 +241,7 @@ public function testUserCannotDeleteLockedQuestion(): void $this->actingAs($this->user) ->from("/") - ->delete("/questions/{$question->id}") + ->delete(route("admin.questions.destroy", $question->id)) ->assertStatus(403); $this->assertDatabaseHas("questions", ["text" => "question"]); @@ -251,7 +251,7 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->delete("/questions/1") + ->delete("/admin/questions/1") ->assertStatus(404); } @@ -267,7 +267,7 @@ public function testUserCanCopyQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); @@ -284,7 +284,7 @@ public function testUserCanCopyLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); @@ -300,7 +300,7 @@ public function testUserCannotCopyAnswerToLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertStatus(403); $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); @@ -318,7 +318,7 @@ public function testUserCanCopyQuestionWithCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertRedirect("/quizzes"); $this->assertNotNull($quizA->questions[0]->correctAnswer); @@ -332,7 +332,7 @@ public function testUserCannotCopyQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/2/clone/{$quiz->id}") + ->post(route("admin.questions.clone", ["question" => 2, "quiz" => $quiz->id])) ->assertStatus(404); } @@ -342,7 +342,7 @@ public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/2") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => 2])) ->assertStatus(404); } } diff --git a/tests/Feature/QuizSubmissionTest.php b/tests/Feature/QuizSubmissionTest.php new file mode 100644 index 00000000..354afc7b --- /dev/null +++ b/tests/Feature/QuizSubmissionTest.php @@ -0,0 +1,63 @@ +user = User::factory()->create(); + } + + public function testUserCanViewSingleSubmission(): void + { + $quiz = Quiz::factory()->create(); + $questions = Question::factory()->count(2)->create(["quiz_id" => $quiz->id]); + Answer::factory()->count(4)->create(["question_id" => $questions[0]->id]); + Answer::factory()->count(4)->create(["question_id" => $questions[1]->id]); + $submission = $quiz->createSubmission($this->user); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 2); + $this->assertDatabaseCount("answers", 8); + $this->assertDatabaseCount("quiz_submissions", 1); + $this->assertDatabaseCount("answer_records", 2); + + $this->actingAs($this->user) + ->get(route("submissions.show", $submission->id)) + ->assertInertia( + fn(Assert $page) => $page + ->component("Submission/Show") + ->where("submission.name", $quiz->name) + ->count("submission.answers", 2) + ->count("submission.answers.0.answers", 4) + ->count("submission.answers.1.answers", 4), + ); + } + + public function testUserCannotViewSubmissionThatIsNotHis(): void + { + $submission = QuizSubmission::factory()->create(); + + $this->actingAs($this->user) + ->get(route("submissions.show", $submission->id)) + ->assertStatus(403); + } +} diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index da397026..09147abc 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -7,7 +7,9 @@ use App\Models\Answer; use App\Models\Question; use App\Models\Quiz; +use App\Models\QuizSubmission; use App\Models\User; +use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; @@ -21,10 +23,16 @@ class QuizTest extends TestCase protected function setUp(): void { parent::setUp(); - + Carbon::setTestNow(Carbon::create(2024, 1, 1, 10)); $this->user = User::factory()->create(); } + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + public function testUserCanViewQuizzes(): void { $quizzes = Quiz::factory()->count(2)->create(); @@ -35,7 +43,7 @@ public function testUserCanViewQuizzes(): void $this->assertDatabaseCount("questions", 10); $this->actingAs($this->user) - ->get("/quizzes") + ->get(route("admin.quizzes.index")) ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Index") @@ -47,7 +55,7 @@ public function testUserCanViewQuizzes(): void public function testUserCannotViewQuizThatNotExisted(): void { - $this->actingAs($this->user)->get("/quizzes/1") + $this->actingAs($this->user)->get(route("admin.quizzes.show", 1)) ->assertStatus(404); } @@ -58,7 +66,7 @@ public function testUserCanViewSingleQuiz(): void $this->assertDatabaseCount("quizzes", 1); $this->actingAs($this->user) - ->get("/quizzes/{$quiz->id}") + ->get(route("admin.quizzes.show", $quiz->id)) ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") @@ -73,7 +81,7 @@ public function testUserCanViewLockedQuiz(): void $this->assertDatabaseCount("quizzes", 1); $this->actingAs($this->user) - ->get("/quizzes/{$quiz->id}") + ->get(route("admin.quizzes.show", $quiz->id)) ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") @@ -86,11 +94,27 @@ public function testUserCanCreateQuiz(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes", ["name" => "Example quiz"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz", "scheduled_at" => "2024-02-10 11:40:00"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("quizzes", [ + "name" => "Example quiz", + "scheduled_at" => "2024-02-10 11:40:00", + ]); + } + + public function testUserCanCreateQuizWithoutDate(): void + { + Carbon::setTestNow(Carbon::create(2024, 1, 1, 10)); + + $this->actingAs($this->user) + ->from("/") + ->post(route("admin.quizzes.store"), ["name" => "Example quiz"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", [ "name" => "Example quiz", + "scheduled_at" => null, ]); } @@ -98,15 +122,15 @@ public function testUserCanCreateMultipleQuizzes(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes", ["name" => "Example quiz 1"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz 1"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes", ["name" => "Example quiz 2"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz 2"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes", ["name" => "Example quiz 3"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz 3"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1"]); @@ -118,33 +142,49 @@ public function testUserCannotCreateInvalidQuiz(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes", []) + ->post(route("admin.quizzes.store"), []) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->post("/quizzes", ["name" => false]) + ->post(route("admin.quizzes.store"), ["name" => false]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); + $this->from("/") + ->post(route("admin.quizzes.store"), ["name" => "correct", "scheduled_at" => "invalid format"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + + $this->from("/") + ->post(route("admin.quizzes.store"), ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + + $this->from("/") + ->post(route("admin.quizzes.store"), ["name" => "correct", "duration" => -100]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + + $this->from("/") + ->post(route("admin.quizzes.store"), ["name" => "correct", "duration" => 0]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + $this->assertDatabaseCount("quizzes", 0); } public function testUserCanEditQuiz(): void { - $quiz = Quiz::factory()->create(["name" => "Old quiz"]); + $quiz = Quiz::factory()->create(["name" => "Old quiz", "scheduled_at" => "2024-02-10 11:40:00"]); $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]) ->assertRedirect("/"); - $this->assertDatabaseHas("quizzes", ["name" => "New quiz"]); + $this->assertDatabaseHas("quizzes", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]); } public function testUserCannotEditQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/1", ["name" => "New quiz"]) + ->patch(route("admin.quizzes.update", 1), ["name" => "New quiz"]) ->assertStatus(404); } @@ -154,13 +194,29 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/{$quiz->id}", []) + ->patch(route("admin.quizzes.update", $quiz->id), []) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->patch("/quizzes/{$quiz->id}", ["name" => true]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => true]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); + $this->from("/") + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "scheduled_at" => "invalid format"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + + $this->from("/") + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + + $this->from("/") + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "duration" => -100]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + + $this->from("/") + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "duration" => 0]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); } @@ -170,7 +226,7 @@ public function testUserCannotEditLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "New quiz"]) ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); @@ -188,7 +244,7 @@ public function testUserCanDeleteQuiz(): void $this->actingAs($this->user) ->from("/") - ->delete("/quizzes/{$quiz->id}") + ->delete(route("admin.quizzes.destroy", $quiz->id)) ->assertRedirect("/"); $this->assertDatabaseMissing("quizzes", ["name" => "quiz"]); @@ -203,7 +259,7 @@ public function testUserCannotDeleteLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->delete("/quizzes/{$quiz->id}") + ->delete(route("admin.quizzes.destroy", $quiz->id)) ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "quiz"]); @@ -213,7 +269,7 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->delete("/quizzes/1") + ->delete(route("admin.quizzes.destroy", 1)) ->assertStatus(404); } @@ -231,7 +287,7 @@ public function testUserCanCopyQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/clone") + ->post(route("admin.quizzes.clone", $quiz->id)) ->assertRedirect("/"); $this->assertDatabaseCount("quizzes", 2); @@ -247,7 +303,7 @@ public function testUserCanCopyLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/clone") + ->post(route("admin.quizzes.clone", $quiz->id)) ->assertRedirect("/"); $this->assertDatabaseCount("quizzes", 2); @@ -257,7 +313,47 @@ public function testUserCannotCopyQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes/2/clone") + ->post(route("admin.quizzes.clone", 2)) ->assertStatus(404); } + + public function testUserCanStartQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(); + + $response = $this->actingAs($this->user) + ->from("/") + ->post(route("quizzes.start", $quiz->id)); + + $submission = QuizSubmission::query()->where([ + "user_id" => $this->user->id, + "quiz_id" => $quiz->id, + ])->firstOrFail(); + + $response->assertRedirect(route("submissions.show", $submission->id)); + } + + public function testUserCannotStartAlreadyStartedQuiz(): void + { + $submission = QuizSubmission::factory()->create(["user_id" => $this->user->id]); + + $this->actingAs($this->user) + ->from("/") + ->post(route("quizzes.start", $submission->quiz->id)) + ->assertRedirect(route("submissions.show", $submission->id)); + + $this->assertDatabaseCount("quiz_submissions", 1); + } + + public function testUserCannotStartUnlockedQuiz(): void + { + $quiz = Quiz::factory()->create(); + + $this->actingAs($this->user) + ->from("/") + ->post(route("quizzes.start", $quiz->id)) + ->assertStatus(403); + + $this->assertDatabaseCount("quiz_submissions", 0); + } }