diff --git a/.env.ci b/.env.ci index c1d1a570..ff188562 100644 --- a/.env.ci +++ b/.env.ci @@ -14,3 +14,10 @@ QUEUE_CONNECTION=sync SESSION_DRIVER=array SESSION_LIFETIME=120 MAIL_MAILER=array + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=interns2024b +DB_USERNAME=interns2024b +DB_PASSWORD=password diff --git a/.gitignore b/.gitignore index 80e45f7c..0bea5fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ google-credentials.json .idea/ .vscode/ .composer +.vite /public/build/ supervisord.pid *.cache diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 77e9631f..ded7c1ff 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,6 +4,9 @@ namespace App\Http\Controllers; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; + abstract class Controller { + use AuthorizesRequests; } diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php new file mode 100644 index 00000000..b1e13205 --- /dev/null +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -0,0 +1,88 @@ + AnswerResource::collection($question->answers), + ]); + } + + public function store(Question $question, AnswerRequest $request): RedirectResponse + { + Answer::query() + ->make($request->validated()) + ->question()->associate($question) + ->save(); + + return redirect() + ->back() + ->with("success", "Answer added successfully"); + } + + public function show(Answer $answer): Response + { + return Inertia::render("Answer/Show", ["answer" => new AnswerResource($answer)]); + } + + public function markAsCorrect(Answer $answer): RedirectResponse + { + $answer->question->correctAnswer()->associate($answer)->save(); + + return redirect() + ->back() + ->with("success", "Answer marked as correct"); + } + + public function markAsInvalid(Answer $answer): RedirectResponse + { + if ($answer->isCorrect) { + $answer->question->correct_answer_id = null; + $answer->save(); + } + + return redirect() + ->back() + ->with("success", "Answer marked as incorrect"); + } + + public function update(AnswerRequest $request, Answer $answer): RedirectResponse + { + $answer->update($request->validated()); + + return redirect() + ->back() + ->with("success", "Answer updated"); + } + + public function destroy(Answer $answer): RedirectResponse + { + $answer->delete(); + + return redirect() + ->back() + ->with("success", "Answer deleted"); + } + + public function clone(Answer $answer, Question $question): RedirectResponse + { + $answer->cloneTo($question); + + return redirect() + ->back() + ->with("success", "Answer cloned"); + } +} diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php new file mode 100644 index 00000000..dad324ac --- /dev/null +++ b/app/Http/Controllers/QuizController.php @@ -0,0 +1,80 @@ +with("questions.answers") + ->get(); + + return Inertia::render("Quiz/Index", ["quizzes" => QuizResource::collection($quizzes)]); + } + + public function store(QuizRequest $request): RedirectResponse + { + Quiz::query()->create($request->validated()); + + return redirect() + ->back() + ->with("success", "Quiz added successfully"); + } + + public function show(int $quiz): Response + { + $quiz = Quiz::query() + ->with("questions.answers") + ->findOrFail($quiz); + + return Inertia::render("Quiz/Show", ["quiz" => new QuizResource($quiz)]); + } + + public function update(QuizRequest $request, Quiz $quiz): RedirectResponse + { + $quiz->update($request->validated()); + + return redirect() + ->back() + ->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(); + + return redirect() + ->back() + ->with("success", "Quiz deleted"); + } + + public function clone(Quiz $quiz): RedirectResponse + { + $quiz->clone(); + + return redirect() + ->back() + ->with("success", "Quiz cloned"); + } +} diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php new file mode 100644 index 00000000..e116d580 --- /dev/null +++ b/app/Http/Controllers/QuizQuestionController.php @@ -0,0 +1,75 @@ +questions() + ->with("answers") + ->get(); + + return Inertia::render("Question/Index", [ + "questions" => QuestionResource::collection($questions), + ]); + } + + public function store(Quiz $quiz, QuestionRequest $request): RedirectResponse + { + Question::query() + ->make($request->validated()) + ->quiz()->associate($quiz) + ->save(); + + return redirect() + ->back() + ->with("success", "Question added successfully"); + } + + public function show(int $question): Response + { + $test = Question::query() + ->with("answers") + ->findOrFail($question); + + return Inertia::render("Question/Show", ["question" => new QuestionResource($test)]); + } + + public function update(QuestionRequest $request, Question $question): RedirectResponse + { + $question->update($request->validated()); + + return redirect() + ->back() + ->with("success", "Question updated"); + } + + public function destroy(Question $question): RedirectResponse + { + $question->delete(); + + return redirect() + ->back() + ->with("success", "Question deleted"); + } + + public function clone(Question $question, Quiz $quiz): RedirectResponse + { + $question->cloneTo($quiz); + + return redirect() + ->back() + ->with("success", "Question cloned"); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 00000000..51156a35 --- /dev/null +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,27 @@ + + */ + public function share(Request $request): array + { + return array_merge(parent::share($request), []); + } +} diff --git a/app/Http/Requests/AnswerRequest.php b/app/Http/Requests/AnswerRequest.php new file mode 100644 index 00000000..677d5a0c --- /dev/null +++ b/app/Http/Requests/AnswerRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + "text" => ["required", "string"], + ]; + } +} diff --git a/app/Http/Requests/QuestionRequest.php b/app/Http/Requests/QuestionRequest.php new file mode 100644 index 00000000..83c80d58 --- /dev/null +++ b/app/Http/Requests/QuestionRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + "text" => ["required", "string"], + ]; + } +} diff --git a/app/Http/Requests/QuizRequest.php b/app/Http/Requests/QuizRequest.php new file mode 100644 index 00000000..ae942518 --- /dev/null +++ b/app/Http/Requests/QuizRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + "name" => ["required", "string"], + ]; + } +} diff --git a/app/Http/Resources/AnswerResource.php b/app/Http/Resources/AnswerResource.php new file mode 100644 index 00000000..1ee6bb24 --- /dev/null +++ b/app/Http/Resources/AnswerResource.php @@ -0,0 +1,22 @@ + $this->id, + "text" => $this->text, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "locked" => $this->isLocked, + "correct" => $this->isCorrect, + ]; + } +} diff --git a/app/Http/Resources/QuestionResource.php b/app/Http/Resources/QuestionResource.php new file mode 100644 index 00000000..b2fd07d6 --- /dev/null +++ b/app/Http/Resources/QuestionResource.php @@ -0,0 +1,23 @@ + $this->id, + "text" => $this->text, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "locked" => $this->isLocked, + "correct" => $this->correctAnswer?->id, + "answers" => AnswerResource::collection($this->answers), + ]; + } +} diff --git a/app/Http/Resources/QuizResource.php b/app/Http/Resources/QuizResource.php new file mode 100644 index 00000000..130fdcf1 --- /dev/null +++ b/app/Http/Resources/QuizResource.php @@ -0,0 +1,22 @@ + $this->id, + "name" => $this->name, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "locked" => $this->isLocked, + "questions" => QuestionResource::collection($this->questions), + ]; + } +} diff --git a/app/Models/Answer.php b/app/Models/Answer.php new file mode 100644 index 00000000..f4f7296f --- /dev/null +++ b/app/Models/Answer.php @@ -0,0 +1,53 @@ +belongsTo(Question::class); + } + + public function isLocked(): Attribute + { + return Attribute::get(fn(): bool => $this->question->isLocked); + } + + 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 new file mode 100644 index 00000000..2bfa1c93 --- /dev/null +++ b/app/Models/Question.php @@ -0,0 +1,72 @@ + $answers + * @property Quiz $quiz + */ +class Question extends Model +{ + use HasFactory; + + protected $fillable = [ + "text", + ]; + + public function correctAnswer(): BelongsTo + { + return $this->belongsTo(Answer::class, "correct_answer_id"); + } + + public function quiz(): BelongsTo + { + return $this->belongsTo(Quiz::class); + } + + public function answers(): HasMany + { + return $this->hasMany(Answer::class); + } + + public function isLocked(): Attribute + { + return Attribute::get(fn(): bool => $this->quiz->isLocked); + } + + 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 new file mode 100644 index 00000000..16eb79d3 --- /dev/null +++ b/app/Models/Quiz.php @@ -0,0 +1,66 @@ + $questions + * @property Collection $answers + */ +class Quiz extends Model +{ + use HasFactory; + + protected $fillable = [ + "name", + ]; + + public function questions(): HasMany + { + return $this->hasMany(Question::class); + } + + public function answers(): HasManyThrough + { + return $this->hasManyThrough(Answer::class, Question::class); + } + + public function isLocked(): Attribute + { + return Attribute::get(fn(): bool => $this->locked_at !== null); + } + + public function clone(): self + { + $quizCopy = $this->replicate(); + $quizCopy->save(); + + foreach ($this->questions as $question) { + $question->cloneTo($quizCopy); + } + + return $quizCopy; + } + + protected function casts(): array + { + return [ + "locked_at" => "datetime", + ]; + } +} diff --git a/app/Policies/AnswerPolicy.php b/app/Policies/AnswerPolicy.php new file mode 100644 index 00000000..09e8a199 --- /dev/null +++ b/app/Policies/AnswerPolicy.php @@ -0,0 +1,32 @@ +isLocked; + } + + public function delete(User $user, Answer $answer): bool + { + return !$answer->isLocked; + } + + public function create(User $user, Question $question): bool + { + return !$question->isLocked; + } + + public function clone(User $user, Answer $answer, Question $question): bool + { + return !$question->isLocked; + } +} diff --git a/app/Policies/QuestionPolicy.php b/app/Policies/QuestionPolicy.php new file mode 100644 index 00000000..f93c15e0 --- /dev/null +++ b/app/Policies/QuestionPolicy.php @@ -0,0 +1,32 @@ +isLocked; + } + + public function delete(User $user, Question $question): bool + { + return !$question->isLocked; + } + + public function create(User $user, Quiz $quiz): bool + { + return !$quiz->isLocked; + } + + public function clone(User $user, Question $question, Quiz $quiz): bool + { + return !$quiz->isLocked; + } +} diff --git a/app/Policies/QuizPolicy.php b/app/Policies/QuizPolicy.php new file mode 100644 index 00000000..510c4332 --- /dev/null +++ b/app/Policies/QuizPolicy.php @@ -0,0 +1,21 @@ +isLocked; + } + + public function delete(User $user, Quiz $quiz): bool + { + return !$quiz->isLocked; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dfdc1c7a..fcd238e8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -14,5 +15,6 @@ public function register(): void public function boot(): void { + JsonResource::withoutWrapping(); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 91570f99..1a0dc62f 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Http\Middleware\HandleInertiaRequests; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; @@ -13,6 +14,7 @@ health: "/up", ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->web(append: HandleInertiaRequests::class); }) ->withExceptions(function (Exceptions $exceptions): void { })->create(); diff --git a/database/factories/AnswerFactory.php b/database/factories/AnswerFactory.php new file mode 100644 index 00000000..ef9f0a0c --- /dev/null +++ b/database/factories/AnswerFactory.php @@ -0,0 +1,30 @@ + + */ +class AnswerFactory extends Factory +{ + public function definition(): array + { + return [ + "text" => fake()->realText(), + "question_id" => Question::factory(), + ]; + } + + public function locked(): static + { + return $this->state(fn(array $attributes): array => [ + "question_id" => Question::factory()->locked(), + ]); + } +} diff --git a/database/factories/QuestionFactory.php b/database/factories/QuestionFactory.php new file mode 100644 index 00000000..cc4f5719 --- /dev/null +++ b/database/factories/QuestionFactory.php @@ -0,0 +1,30 @@ + + */ +class QuestionFactory extends Factory +{ + public function definition(): array + { + return [ + "text" => fake()->realText(), + "quiz_id" => Quiz::factory(), + ]; + } + + public function locked(): static + { + return $this->state(fn(array $attributes): array => [ + "quiz_id" => Quiz::factory()->locked(), + ]); + } +} diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php new file mode 100644 index 00000000..be5b4a31 --- /dev/null +++ b/database/factories/QuizFactory.php @@ -0,0 +1,29 @@ + + */ +class QuizFactory extends Factory +{ + public function definition(): array + { + return [ + "name" => fake()->name(), + ]; + } + + public function locked(): static + { + return $this->state(fn(array $attributes): array => [ + "locked_at" => Carbon::now(), + ]); + } +} diff --git a/database/migrations/2024_08_08_123620_create_quizzes_table.php b/database/migrations/2024_08_08_123620_create_quizzes_table.php new file mode 100644 index 00000000..c8a60917 --- /dev/null +++ b/database/migrations/2024_08_08_123620_create_quizzes_table.php @@ -0,0 +1,24 @@ +id(); + $table->timestamps(); + $table->timestamp("locked_at")->nullable(); + $table->string("name"); + }); + } + + public function down(): void + { + Schema::dropIfExists("quizzes"); + } +}; diff --git a/database/migrations/2024_08_08_123627_create_questions_table.php b/database/migrations/2024_08_08_123627_create_questions_table.php new file mode 100644 index 00000000..842358c1 --- /dev/null +++ b/database/migrations/2024_08_08_123627_create_questions_table.php @@ -0,0 +1,25 @@ +id(); + $table->timestamps(); + $table->text("text"); + $table->foreignIdFor(Quiz::class)->constrained()->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists("questions"); + } +}; diff --git a/database/migrations/2024_08_08_162910_create_answers_table.php b/database/migrations/2024_08_08_162910_create_answers_table.php new file mode 100644 index 00000000..7cd314be --- /dev/null +++ b/database/migrations/2024_08_08_162910_create_answers_table.php @@ -0,0 +1,25 @@ +id(); + $table->timestamps(); + $table->text("text"); + $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists("answers"); + } +}; diff --git a/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php b/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php new file mode 100644 index 00000000..5b6ed6d0 --- /dev/null +++ b/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php @@ -0,0 +1,24 @@ +foreignIdFor(Answer::class, "correct_answer_id")->nullable()->constrained("answers")->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table("questions", function (Blueprint $table): void { + $table->dropColumn("correct_answer_id"); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index de853239..cff3b9ec 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,11 +4,15 @@ namespace Database\Seeders; +use App\Models\Answer; +use App\Models\Quiz; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { + Quiz::factory()->create(); + Answer::factory()->create(); } } diff --git a/phpunit.xml b/phpunit.xml index fe6dd827..b5ee5401 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,7 +20,7 @@ - + diff --git a/resources/js/Pages/Answer/Index.vue b/resources/js/Pages/Answer/Index.vue new file mode 100644 index 00000000..105a879f --- /dev/null +++ b/resources/js/Pages/Answer/Index.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Answer/Show.vue b/resources/js/Pages/Answer/Show.vue new file mode 100644 index 00000000..cf4bece0 --- /dev/null +++ b/resources/js/Pages/Answer/Show.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/Pages/Question/Index.vue b/resources/js/Pages/Question/Index.vue new file mode 100644 index 00000000..44657e5d --- /dev/null +++ b/resources/js/Pages/Question/Index.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Question/Show.vue b/resources/js/Pages/Question/Show.vue new file mode 100644 index 00000000..e1a12ba3 --- /dev/null +++ b/resources/js/Pages/Question/Show.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/Pages/Quiz/Index.vue b/resources/js/Pages/Quiz/Index.vue new file mode 100644 index 00000000..c93133f3 --- /dev/null +++ b/resources/js/Pages/Quiz/Index.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Quiz/Show.vue b/resources/js/Pages/Quiz/Show.vue new file mode 100644 index 00000000..b59f92c1 --- /dev/null +++ b/resources/js/Pages/Quiz/Show.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/Types/Answer.d.ts b/resources/js/Types/Answer.d.ts new file mode 100644 index 00000000..f14cd96c --- /dev/null +++ b/resources/js/Types/Answer.d.ts @@ -0,0 +1,8 @@ +export interface Answer { + id: number + text: string + createdAt: number + updatedAt: number + locked: boolean + correct: boolean +} diff --git a/resources/js/Types/Question.d.ts b/resources/js/Types/Question.d.ts new file mode 100644 index 00000000..48ededb4 --- /dev/null +++ b/resources/js/Types/Question.d.ts @@ -0,0 +1,11 @@ +import {type Answer} from '@/Types/Answer' + +export interface Question { + id: number + text: string + createdAt: number + updatedAt: number + locked: boolean + correct?: number + answers: Answer[] +} diff --git a/resources/js/Types/Quiz.d.ts b/resources/js/Types/Quiz.d.ts new file mode 100644 index 00000000..197c246e --- /dev/null +++ b/resources/js/Types/Quiz.d.ts @@ -0,0 +1,10 @@ +import {type Question} from '@/Types/Question' + +export interface Quiz { + id: number + name: string + createdAt: number + updatedAt: number + locked: boolean + questions: Question[] +} diff --git a/routes/web.php b/routes/web.php index 2301789d..710fd4fa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,7 +2,36 @@ declare(strict_types=1); +use App\Http\Controllers\QuestionAnswerController; +use App\Http\Controllers\QuizController; +use App\Http\Controllers\QuizQuestionController; +use App\Models\Answer; +use App\Models\Question; use Illuminate\Support\Facades\Route; use Inertia\Response; Route::get("/", fn(): Response => inertia("Welcome")); + +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::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("/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"); diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php new file mode 100644 index 00000000..6546052c --- /dev/null +++ b/tests/Feature/AnswerTest.php @@ -0,0 +1,438 @@ +user = User::factory()->create(); + } + + public function testUserCanViewQuestionAnswers(): void + { + $question = Question::factory()->create(); + Answer::factory()->count(10)->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($this->user) + ->get("/questions/{$question->id}/answers") + ->assertInertia( + fn(Assert $page) => $page + ->component("Answer/Index") + ->has("answers", 10), + ); + } + + public function testUserCannotViewAnswersOfQuestionThatNotExisted(): void + { + $this->actingAs($this->user)->get("/questions/1/answers") + ->assertStatus(404); + } + + public function testUserCanViewSingleAnswer(): void + { + $answer = Answer::factory()->create(); + + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($this->user) + ->get("/answers/{$answer->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Answer/Show") + ->where("answer.id", $answer->id), + ); + } + + public function testUserCanViewLockedAnswer(): void + { + $answer = Answer::factory()->locked()->create(); + + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($this->user) + ->get("/answers/{$answer->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Answer/Show") + ->where("answer.id", $answer->id) + ->where("answer.locked", true), + ); + } + + public function testUserCannotViewAnswerThatNotExisted(): void + { + $this->actingAs($this->user)->get("/answers/1") + ->assertStatus(404); + } + + public function testUserCanCreateAnswer(): void + { + $question = Question::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer"]) + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", [ + "text" => "Example answer", + "question_id" => $question->id, + ]); + } + + public function testUserCanCreateMultipleAnswers(): void + { + $question = Question::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->assertRedirect("/quizzes"); + + $this->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 2"]) + ->assertRedirect("/quizzes"); + + $this->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 3"]) + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["text" => "Example answer 1"]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 2"]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 2"]); + } + + public function testUserCannotCreateAnswerToQuestionThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/1/answers", ["text" => "Example answer"]) + ->assertStatus(404); + + $this->assertDatabaseMissing("answers", [ + "text" => "Example answer", + ]); + } + + public function testUserCannotCreateAnswerToQuestionThatIsLocked(): void + { + $question = Question::factory()->locked()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->assertStatus(403); + + $this->assertDatabaseMissing("answers", [ + "text" => "Example answer", + ]); + } + + public function testUserCannotCreateInvalidAnswer(): void + { + $question = Question::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", []) + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); + + $this->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => false]) + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); + + $this->assertDatabaseCount("answers", 0); + } + + public function testUserCanEditAnswer(): void + { + $answer = Answer::factory()->create(["text" => "Old answer"]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["text" => "New answer"]); + } + + public function testUserCannotEditAnswerThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/quizzes") + ->patch("/answers/1", ["text" => "New answer"]) + ->assertStatus(404); + } + + public function testUserCannotMakeInvalidEdit(): void + { + $answer = Answer::factory()->create(["text" => "Old answer"]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->patch("/answers/{$answer->id}", []) + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); + + $this->from("/quizzes") + ->patch("/answers/{$answer->id}", ["text" => true]) + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); + + $this->assertDatabaseHas("answers", ["text" => "Old answer"]); + } + + public function testUserCannotEditLockedAnswer(): void + { + $answer = Answer::factory()->locked()->create(["text" => "Old answer"]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->assertStatus(403); + + $this->assertDatabaseHas("answers", ["text" => "Old answer"]); + } + + public function testUserCanDeleteAnswer(): void + { + $answer = Answer::factory()->create(["text" => "answer"]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->delete("/answers/{$answer->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseMissing("answers", ["text" => "answer"]); + } + + public function testUserCannotDeleteLockedAnswer(): void + { + $answer = Answer::factory()->locked()->create(["text" => "answer"]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->delete("/answers/{$answer->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("answers", ["text" => "answer"]); + } + + public function testUserCannotDeleteAnswerThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/quizzes") + ->delete("/answers/1") + ->assertStatus(404); + } + + public function testUserCanMarkAnswerAsCorrect(): void + { + $answer = Answer::factory()->create(["text" => "answer"]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/correct") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + } + + public function testUserCanChangeCorrectAnswer(): void + { + $question = Question::factory()->create(); + $answerA = Answer::factory()->create(["text" => "answer A", "question_id" => $question->id]); + $answerB = Answer::factory()->create(["text" => "answer B", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answerA); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answerB->id}/correct") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerB->id]); + } + + public function testUserCanDeleteCorrectAnswer(): void + { + $question = Question::factory()->create(); + $answer = Answer::factory()->create(["text" => "answer", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->delete("/answers/{$answer->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => null]); + } + + public function testUserCannotChangeCorrectAnswerInLockedQuestion(): void + { + $question = Question::factory()->locked()->create(); + $answerA = Answer::factory()->create(["text" => "answer A", "question_id" => $question->id]); + $answerB = Answer::factory()->create(["text" => "answer B", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answerA); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answerB->id}/correct") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); + } + + public function testUserCanChangeCorrectAnswerToInvalid(): void + { + $question = Question::factory()->create(); + $answer = Answer::factory()->create(["text" => "answer", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/invalid") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + } + + public function testUserCannotChangeCorrectAnswerToInvalidInLockedQuestion(): void + { + $question = Question::factory()->locked()->create(); + $answer = Answer::factory()->create(["text" => "answer", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/invalid") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + } + + public function testUserCanCopyAnswer(): void + { + $questionA = Question::factory()->create(); + $questionB = Question::factory()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); + } + + public function testUserCanCopyLockedAnswer(): void + { + $questionA = Question::factory()->locked()->create(); + $questionB = Question::factory()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); + } + + public function testUserCannotCopyAnswerToLockedQuestion(): void + { + $questionA = Question::factory()->create(); + $questionB = Question::factory()->locked()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + } + + public function testUserCanCopyCorrectAnswer(): void + { + $questionA = Question::factory()->create(); + $questionB = Question::factory()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $questionA->correctAnswer()->associate($answer); + $questionA->save(); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + $this->assertDatabaseHas("questions", ["id" => $questionA->id, "correct_answer_id" => $answer->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); + $this->assertDatabaseHas("questions", ["id" => $questionA->id, "correct_answer_id" => $answer->id]); + $this->assertDatabaseHas("questions", ["id" => $questionB->id, "correct_answer_id" => null]); + } + + public function testUserCannotCopyAnswerThatNotExisted(): void + { + $question = Question::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/2/clone/{$question->id}") + ->assertStatus(404); + } + + public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void + { + $answer = Answer::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/2") + ->assertStatus(404); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 55a42c3a..00000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,17 +0,0 @@ -get("/"); - - $response->assertStatus(200); - } -} diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php new file mode 100644 index 00000000..673235a1 --- /dev/null +++ b/tests/Feature/QuestionTest.php @@ -0,0 +1,348 @@ +user = User::factory()->create(); + } + + public function testUserCanViewQuizQuestions(): void + { + $quiz = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quiz->id]); + Answer::factory()->count(10)->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 1); + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($this->user) + ->get("/quizzes/{$quiz->id}/questions") + ->assertInertia( + fn(Assert $page) => $page + ->component("Question/Index") + ->has("questions", 1) + ->has("questions.0.answers", 10), + ); + } + + public function testUserCannotViewQuestionsOfQuizThatNotExisted(): void + { + $this->actingAs($this->user)->get("/quizzes/1/questions") + ->assertStatus(404); + } + + public function testUserCanViewSingleQuestion(): void + { + $question = Question::factory()->create(); + + $this->assertDatabaseCount("questions", 1); + + $this->actingAs($this->user) + ->get("/questions/{$question->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Question/Show") + ->where("question.id", $question->id), + ); + } + + public function testUserCanViewLockedQuestion(): void + { + $question = Question::factory()->locked()->create(); + + $this->assertDatabaseCount("questions", 1); + + $this->actingAs($this->user) + ->get("/questions/{$question->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Question/Show") + ->where("question.id", $question->id) + ->where("question.locked", true), + ); + } + + public function testUserCannotViewQuestionThatNotExisted(): void + { + $this->actingAs($this->user)->get("/questions/1") + ->assertStatus(404); + } + + public function testUserCanCreateQuestion(): void + { + $quiz = Quiz::factory()->create(); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("questions", [ + "text" => "Example question", + "quiz_id" => $quiz->id, + ]); + } + + public function testUserCanCreateMultipleQuestions(): void + { + $quiz = Quiz::factory()->create(); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->assertRedirect("/"); + + $this->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) + ->assertRedirect("/"); + + $this->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("questions", ["text" => "Example question 1"]); + $this->assertDatabaseHas("questions", ["text" => "Example question 2"]); + $this->assertDatabaseHas("questions", ["text" => "Example question 2"]); + } + + public function testUserCannotCreateQuestionToQuizThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/1/questions", ["text" => "Example question"]) + ->assertStatus(404); + + $this->assertDatabaseMissing("questions", [ + "text" => "Example question", + ]); + } + + public function testUserCannotCreateQuestionToQuizThatIsLocked(): void + { + $quiz = Quiz::factory()->locked()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->assertStatus(403); + + $this->assertDatabaseMissing("questions", [ + "text" => "Example question", + ]); + } + + public function testUserCannotCreateInvalidQuestion(): void + { + $quiz = Quiz::factory()->create(); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/questions", []) + ->assertRedirect("/")->assertSessionHasErrors(["text"]); + + $this->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => false]) + ->assertRedirect("/")->assertSessionHasErrors(["text"]); + + $this->assertDatabaseCount("questions", 0); + } + + public function testUserCanEditQuestion(): void + { + $question = Question::factory()->create(["text" => "Old questions"]); + + $this->actingAs($this->user) + ->from("/") + ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("questions", ["text" => "New question"]); + } + + public function testUserCannotEditQuestionThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/") + ->patch("/questions/1", ["text" => "New question"]) + ->assertStatus(404); + } + + public function testUserCannotMakeInvalidEdit(): void + { + $question = Question::factory()->create(["text" => "Old questions"]); + + $this->actingAs($this->user) + ->from("/") + ->patch("/questions/{$question->id}", []) + ->assertRedirect("/")->assertSessionHasErrors(["text"]); + + $this->from("/") + ->patch("/questions/{$question->id}", ["text" => true]) + ->assertRedirect("/")->assertSessionHasErrors(["text"]); + + $this->assertDatabaseHas("questions", ["text" => "Old questions"]); + } + + public function testUserCannotEditLockedQuestion(): void + { + $question = Question::factory()->locked()->create(["text" => "Old question"]); + + $this->actingAs($this->user) + ->from("/") + ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["text" => "Old question"]); + } + + public function testUserCanDeleteQuestion(): void + { + $question = Question::factory()->create(["text" => "question"]); + Answer::factory()->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 1); + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($this->user) + ->from("/") + ->delete("/questions/{$question->id}") + ->assertRedirect("/"); + + $this->assertDatabaseMissing("questions", ["text" => "question"]); + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 0); + $this->assertDatabaseCount("answers", 0); + } + + public function testUserCannotDeleteLockedQuestion(): void + { + $question = Question::factory()->locked()->create(["text" => "question"]); + + $this->actingAs($this->user) + ->from("/") + ->delete("/questions/{$question->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["text" => "question"]); + } + + public function testUserCannotDeleteQuestionThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/") + ->delete("/questions/1") + ->assertStatus(404); + } + + public function testUserCanCopyQuestion(): void + { + $quizA = Quiz::factory()->create(); + $quizB = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + Answer::factory()->count(10)->create(["question_id" => $question->id]); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); + $this->assertDatabaseCount("answers", 20); + } + + public function testUserCanCopyLockedQuestion(): void + { + $quizA = Quiz::factory()->locked()->create(); + $quizB = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); + } + + public function testUserCannotCopyAnswerToLockedQuestion(): void + { + $quizA = Quiz::factory()->create(); + $quizB = Quiz::factory()->locked()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + } + + public function testUserCanCopyQuestionWithCorrectAnswer(): void + { + $quizA = Quiz::factory()->create(); + $quizB = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + $answer = Answer::factory()->create(["text" => "correct", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertRedirect("/quizzes"); + + $this->assertNotNull($quizA->questions[0]->correctAnswer); + $this->assertNotNull($quizB->questions[0]->correctAnswer); + $this->assertNotEquals($quizA->questions[0]->correctAnswer->id, $quizB->questions[0]->correctAnswer->id); + } + + public function testUserCannotCopyQuestionThatNotExisted(): void + { + $quiz = Question::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/2/clone/{$quiz->id}") + ->assertStatus(404); + } + + public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void + { + $question = Question::factory()->create(); + + $this->actingAs($this->user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/2") + ->assertStatus(404); + } +} diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php new file mode 100644 index 00000000..da397026 --- /dev/null +++ b/tests/Feature/QuizTest.php @@ -0,0 +1,263 @@ +user = User::factory()->create(); + } + + public function testUserCanViewQuizzes(): void + { + $quizzes = Quiz::factory()->count(2)->create(); + Question::factory()->count(5)->create(["quiz_id" => $quizzes[0]->id]); + Question::factory()->count(5)->create(["quiz_id" => $quizzes[1]->id]); + + $this->assertDatabaseCount("quizzes", 2); + $this->assertDatabaseCount("questions", 10); + + $this->actingAs($this->user) + ->get("/quizzes") + ->assertInertia( + fn(Assert $page) => $page + ->component("Quiz/Index") + ->has("quizzes", 2) + ->has("quizzes.0.questions", 5) + ->has("quizzes.1.questions", 5), + ); + } + + public function testUserCannotViewQuizThatNotExisted(): void + { + $this->actingAs($this->user)->get("/quizzes/1") + ->assertStatus(404); + } + + public function testUserCanViewSingleQuiz(): void + { + $quiz = Quiz::factory()->create(); + + $this->assertDatabaseCount("quizzes", 1); + + $this->actingAs($this->user) + ->get("/quizzes/{$quiz->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Quiz/Show") + ->where("quiz.id", $quiz->id), + ); + } + + public function testUserCanViewLockedQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(); + + $this->assertDatabaseCount("quizzes", 1); + + $this->actingAs($this->user) + ->get("/quizzes/{$quiz->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Quiz/Show") + ->where("quiz.id", $quiz->id) + ->where("quiz.locked", true), + ); + } + + public function testUserCanCreateQuiz(): void + { + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes", ["name" => "Example quiz"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("quizzes", [ + "name" => "Example quiz", + ]); + } + + public function testUserCanCreateMultipleQuizzes(): void + { + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes", ["name" => "Example quiz 1"]) + ->assertRedirect("/"); + + $this->from("/") + ->post("/quizzes", ["name" => "Example quiz 2"]) + ->assertRedirect("/"); + + $this->from("/") + ->post("/quizzes", ["name" => "Example quiz 3"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1"]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2"]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2"]); + } + + public function testUserCannotCreateInvalidQuiz(): void + { + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes", []) + ->assertRedirect("/")->assertSessionHasErrors(["name"]); + + $this->from("/") + ->post("/quizzes", ["name" => false]) + ->assertRedirect("/")->assertSessionHasErrors(["name"]); + + $this->assertDatabaseCount("quizzes", 0); + } + + public function testUserCanEditQuiz(): void + { + $quiz = Quiz::factory()->create(["name" => "Old quiz"]); + + $this->actingAs($this->user) + ->from("/") + ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("quizzes", ["name" => "New quiz"]); + } + + public function testUserCannotEditQuizThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/") + ->patch("/quizzes/1", ["name" => "New quiz"]) + ->assertStatus(404); + } + + public function testUserCannotMakeInvalidEdit(): void + { + $quiz = Quiz::factory()->create(["name" => "Old quiz"]); + + $this->actingAs($this->user) + ->from("/") + ->patch("/quizzes/{$quiz->id}", []) + ->assertRedirect("/")->assertSessionHasErrors(["name"]); + + $this->from("/") + ->patch("/quizzes/{$quiz->id}", ["name" => true]) + ->assertRedirect("/")->assertSessionHasErrors(["name"]); + + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); + } + + public function testUserCannotEditLockedQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(["name" => "Old quiz"]); + + $this->actingAs($this->user) + ->from("/") + ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->assertStatus(403); + + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); + } + + public function testUserCanDeleteQuiz(): void + { + $quiz = Quiz::factory()->create(["name" => "quiz"]); + $question = Question::factory()->create(["quiz_id" => $quiz->id]); + Answer::factory()->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 1); + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($this->user) + ->from("/") + ->delete("/quizzes/{$quiz->id}") + ->assertRedirect("/"); + + $this->assertDatabaseMissing("quizzes", ["name" => "quiz"]); + $this->assertDatabaseCount("quizzes", 0); + $this->assertDatabaseCount("questions", 0); + $this->assertDatabaseCount("answers", 0); + } + + public function testUserCannotDeleteLockedQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(["name" => "quiz"]); + + $this->actingAs($this->user) + ->from("/") + ->delete("/quizzes/{$quiz->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("quizzes", ["name" => "quiz"]); + } + + public function testUserCannotDeleteQuestionThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/") + ->delete("/quizzes/1") + ->assertStatus(404); + } + + public function testUserCanCopyQuiz(): void + { + $quiz = Quiz::factory()->create(); + $questions = Question::factory()->count(2)->create(["quiz_id" => $quiz->id]); + + Answer::factory()->count(10)->create(["question_id" => $questions[0]->id]); + Answer::factory()->count(10)->create(["question_id" => $questions[1]->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 2); + $this->assertDatabaseCount("answers", 20); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/clone") + ->assertRedirect("/"); + + $this->assertDatabaseCount("quizzes", 2); + $this->assertDatabaseCount("questions", 4); + $this->assertDatabaseCount("answers", 40); + } + + public function testUserCanCopyLockedQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(); + + $this->assertDatabaseCount("quizzes", 1); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/clone") + ->assertRedirect("/"); + + $this->assertDatabaseCount("quizzes", 2); + } + + public function testUserCannotCopyQuizThatNotExisted(): void + { + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/2/clone") + ->assertStatus(404); + } +}