From 3f7632911d64bd87ad8ab521cf9b5e9170645f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Purga=C5=82?= Date: Sun, 15 Dec 2024 17:45:05 +0100 Subject: [PATCH 1/2] - Private tests (#129) * create mail for close quiz action * add queue:listen to makefile * remove duplicate recipes * split quiz controller logic into action files * add tests for actions * test user model * remove randomness from test * fix command name * implement sending notification when quiz is closed * fix wasClosedManually method * prepare quizzes for testing * fix filterArchivedQuizzes method * add archived quiz to seeder * fix code style * fix no answer warning * change number to int * add return type * move isClosingToday logic to sql * revert to non-SQL version * Revert "- Update all non-major dependencies with digest and pinDigest (#122)" This reverts commit 1a208867877a09df8481f943092c8ada408de801. * fix namespaces * fix code style * replace nunomaduro/larastan with larastan/larastan * wip * fix test * fix test * fix quiz header * update isPublic translation --- app/Http/Controllers/ContestController.php | 4 + app/Http/Requests/QuizRequest.php | 5 ++ app/Http/Requests/UpdateQuizRequest.php | 5 ++ app/Http/Resources/QuizResource.php | 1 + app/Models/Quiz.php | 4 +- app/Policies/QuizPolicy.php | 4 +- database/factories/QuizFactory.php | 8 ++ ...2024_08_08_123620_create_quizzes_table.php | 1 + resources/js/Types/Quiz.d.ts | 1 + .../js/components/QuizzesPanel/QuizHeader.vue | 49 ++++++++++-- tests/Feature/QuizTest.php | 80 ++++++++++++++++++- 11 files changed, 149 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/ContestController.php b/app/Http/Controllers/ContestController.php index 8d9ef008..06b06ba3 100644 --- a/app/Http/Controllers/ContestController.php +++ b/app/Http/Controllers/ContestController.php @@ -39,8 +39,12 @@ public function create(Request $request): RedirectResponse|Response ->with(["userQuestions.question.answers", "quiz"]) ->get(); + $assignedQuizzes = $user->assignedQuizzes->pluck("id"); + $quizzes = Quiz::query() ->whereNotNull("locked_at") + ->where("is_public", true) + ->orWhereIn("id", $assignedQuizzes) ->with("questions.answers") ->get(); diff --git a/app/Http/Requests/QuizRequest.php b/app/Http/Requests/QuizRequest.php index a17e24e9..c5b613e7 100644 --- a/app/Http/Requests/QuizRequest.php +++ b/app/Http/Requests/QuizRequest.php @@ -19,6 +19,10 @@ public function prepareForValidation(): void if ($this->has("scheduledAt")) { $this->merge(["scheduled_at" => $this->input("scheduledAt")]); } + + if ($this->has("isPublic")) { + $this->merge(["is_public" => $this->input("isPublic")]); + } } /** @@ -31,6 +35,7 @@ public function rules(): array "scheduled_at" => ["date", "after:now"], "duration" => ["numeric", "min:1", "max:2147483647"], "description" => ["string", "nullable"], + "is_public" => ["boolean"], ]; } } diff --git a/app/Http/Requests/UpdateQuizRequest.php b/app/Http/Requests/UpdateQuizRequest.php index 04c00e90..b888323d 100644 --- a/app/Http/Requests/UpdateQuizRequest.php +++ b/app/Http/Requests/UpdateQuizRequest.php @@ -20,6 +20,10 @@ public function prepareForValidation(): void if ($this->has("scheduledAt")) { $this->merge(["scheduled_at" => $this->input("scheduledAt")]); } + + if ($this->has("isPublic")) { + $this->merge(["is_public" => $this->input("isPublic")]); + } } /** @@ -30,6 +34,7 @@ public function rules(): array return [ "title" => ["required", "string", "max:255"], "scheduled_at" => ["date", "after:now"], + "is_public" => ["boolean"], "duration" => ["integer", "min:1", "max:2147483647"], "description" => ["string", "nullable"], "questions" => ["array"], diff --git a/app/Http/Resources/QuizResource.php b/app/Http/Resources/QuizResource.php index dc57fa6b..43e9ec51 100644 --- a/app/Http/Resources/QuizResource.php +++ b/app/Http/Resources/QuizResource.php @@ -19,6 +19,7 @@ public function toArray($request): array "scheduledAt" => $this->scheduled_at, "duration" => $this->duration, "state" => $this->state, + "isPublic" => $this->is_public, "canBeLocked" => $this->canBeLocked, "canBeUnlocked" => $this->canBeUnlocked, "questions" => $this->is_local ? [] : QuestionResource::collection($this->questions), diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 7d8acc1b..5625b187 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -22,13 +22,14 @@ * @property ?Carbon $ranking_published_at * @property ?Carbon $locked_at * @property bool $is_local + * @property bool $is_public * @property ?int $duration + * @property ?string $description * @property bool $isLocked * @property bool $isPublished * @property bool $canBeLocked * @property bool $canBeUnlocked * @property string $state - * @property ?string $description * @property bool $isRankingPublished * @property ?Carbon $closeAt * @property Collection $questions @@ -46,6 +47,7 @@ class Quiz extends Model "duration", "ranking_published_at", "description", + "is_public", ]; protected $guarded = []; diff --git a/app/Policies/QuizPolicy.php b/app/Policies/QuizPolicy.php index 1b5cc123..c3a38209 100644 --- a/app/Policies/QuizPolicy.php +++ b/app/Policies/QuizPolicy.php @@ -27,7 +27,7 @@ public function delete(User $user, Quiz $quiz): bool public function submit(User $user, Quiz $quiz): bool { - return $quiz->isLocked && !$quiz->is_local; + return $quiz->isLocked && !$quiz->is_local && ($quiz->is_public || $quiz->assignedUsers()->where("user_id", $user->id)->exists()); } public function lock(User $user, Quiz $quiz): bool @@ -42,7 +42,7 @@ public function unlock(User $user, Quiz $quiz): bool public function assign(User $user, Quiz $quiz): bool { - return $quiz->isLocked && !$quiz->isPublished && !$quiz->hasUserQuizzesFrom($user); + return $quiz->isLocked && !$quiz->isPublished && $quiz->is_public && !$quiz->hasUserQuizzesFrom($user); } public function viewAdminRanking(User $user, Quiz $quiz): Response diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index 13081b72..fe7ad71d 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -17,6 +17,7 @@ public function definition(): array { return [ "title" => fake()->name(), + "is_public" => true, "duration" => fake()->numberBetween(60, 120), ]; } @@ -47,6 +48,13 @@ public function withRanking(): static ]); } + public function private(): static + { + return $this->state(fn(array $attributes): array => [ + "is_public" => false, + ]); + } + public function local(): static { return $this->state(fn(array $attributes): array => [ diff --git a/database/migrations/2024_08_08_123620_create_quizzes_table.php b/database/migrations/2024_08_08_123620_create_quizzes_table.php index 28022d06..755903c5 100644 --- a/database/migrations/2024_08_08_123620_create_quizzes_table.php +++ b/database/migrations/2024_08_08_123620_create_quizzes_table.php @@ -15,6 +15,7 @@ public function up(): void $table->timestamp("locked_at")->nullable(); $table->string("title"); $table->text("description")->nullable(); + $table->boolean("is_public")->default(false); }); } diff --git a/resources/js/Types/Quiz.d.ts b/resources/js/Types/Quiz.d.ts index 11fbd438..30093a9b 100644 --- a/resources/js/Types/Quiz.d.ts +++ b/resources/js/Types/Quiz.d.ts @@ -11,6 +11,7 @@ interface Quiz { description?: string isUserAssigned: boolean isRankingPublished: boolean + isPublic: boolean questions: Question[] } diff --git a/resources/js/components/QuizzesPanel/QuizHeader.vue b/resources/js/components/QuizzesPanel/QuizHeader.vue index de6b7fc2..72007b5c 100644 --- a/resources/js/components/QuizzesPanel/QuizHeader.vue +++ b/resources/js/components/QuizzesPanel/QuizHeader.vue @@ -42,17 +42,50 @@ const quiz = defineModel({ required: true }) + +
- +

Widoczność:

+
diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index 075f1d1b..f9c945be 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -83,7 +83,7 @@ public function testLocalQuizHasNoQuestionsVisible(): void ); } - public function testUserCanCreateQuiz(): void + public function testAdminCanCreateQuiz(): void { $this->actingAs($this->admin) ->from("/") @@ -180,13 +180,14 @@ public function testAdminCannotCreateInvalidQuiz(): void public function testAdminCanEditQuiz(): void { - $quiz = Quiz::factory()->create(["title" => "Old quiz", "scheduled_at" => "2024-02-10 11:40:00"]); + $quiz = Quiz::factory()->create(["title" => "Old quiz", "is_public" => false, "scheduled_at" => "2024-02-10 11:40:00"]); $question = Question::factory()->create(["quiz_id" => $quiz->id]); $data = [ "title" => "Quiz Name", "scheduled_at" => "2024-08-28 15:00:00", "duration" => 120, + "is_public" => true, "description" => "test", "questions" => [ [ @@ -207,6 +208,7 @@ public function testAdminCanEditQuiz(): void "description" => "test", "scheduled_at" => "2024-08-28 15:00:00", "duration" => 120, + "is_public" => true, ]); $this->assertDatabaseHas("questions", [ @@ -586,6 +588,80 @@ public function testUserCanStartQuiz(): void $response->assertRedirect("/quizzes/$userQuiz->id/"); } + public function testUninvitedUserCannotStartPrivateQuiz(): void + { + $quiz = Quiz::factory()->locked()->private()->create([ + "scheduled_at" => Carbon::now()->subMinutes(60), + ]); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/start") + ->assertStatus(403); + + $this->assertDatabaseCount("user_quizzes", 0); + } + + public function testInvitedUserCanStartPrivateQuiz(): void + { + $quiz = Quiz::factory()->locked()->private()->create([ + "scheduled_at" => Carbon::now()->subMinutes(60), + ]); + + $quiz->assignedUsers()->attach($this->user); + + $response = $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/start"); + + $userQuiz = UserQuiz::query()->where([ + "user_id" => $this->user->id, + "quiz_id" => $quiz->id, + ])->firstOrFail(); + + $response->assertRedirect("/quizzes/$userQuiz->id/"); + } + + public function testInvitedUserCanViewPublicQuiz(): void + { + Quiz::factory()->locked()->create(); + + $this->actingAs($this->user) + ->from("/") + ->get("/dashboard") + ->assertInertia( + fn(Assert $page) => $page->component("User/Dashboard") + ->has("quizzes", 1), + ); + } + + public function testInvitedUserCanViewPrivateQuiz(): void + { + $quiz = Quiz::factory()->locked()->private()->create(); + $quiz->assignedUsers()->attach($this->user); + + $this->actingAs($this->user) + ->from("/") + ->get("/dashboard") + ->assertInertia( + fn(Assert $page) => $page->component("User/Dashboard") + ->has("quizzes", 1), + ); + } + + public function testUninvitedUserCannotViewPrivateQuiz(): void + { + Quiz::factory()->private()->create(); + + $this->actingAs($this->user) + ->from("/") + ->get("/dashboard") + ->assertInertia( + fn(Assert $page) => $page->component("User/Dashboard") + ->has("quizzes", 0), + ); + } + public function testUserCannotStartAlreadyStartedQuiz(): void { $userQuiz = UserQuiz::factory()->create(["user_id" => $this->user->id]); From 1c832739a21de92ea5577176094cbd0a0563deb8 Mon Sep 17 00:00:00 2001 From: Dominikaninn <130690231+PrabuckiDominik@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:49:51 +0100 Subject: [PATCH 2/2] #88 - Implement force change password for admin (#130) * Add force password change to migration, action * Add middleware and implement force change password with tests --- app/Actions/CreateAdminAction.php | 27 ++++++++++++ app/Actions/ForceChangePasswordAction.php | 16 +++++++ app/Http/Controllers/AdminController.php | 23 +++------- .../Controllers/ProfileUserController.php | 3 ++ .../Controllers/QuizQuestionController.php | 2 +- .../EnsureUserPasswordWasChanged.php | 25 +++++++++++ .../Requests/Auth/RegisterAdminRequest.php | 2 +- app/Http/Resources/UserResource.php | 1 + .../CheckPasswordWasForceChanged.php | 19 ++++++++ app/Models/User.php | 5 ++- ...d_force_password_change_to_users_table.php | 23 ++++++++++ resources/js/Types/User.d.ts | 1 + routes/web.php | 9 ++-- tests/Feature/ResetPasswordTest.php | 21 +++++++++ tests/Unit/CreateAdminActionTest.php | 43 +++++++++++++++++++ tests/Unit/ForceChangePasswordActionTest.php | 35 +++++++++++++++ 16 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 app/Actions/CreateAdminAction.php create mode 100644 app/Actions/ForceChangePasswordAction.php create mode 100644 app/Http/Middleware/EnsureUserPasswordWasChanged.php create mode 100644 app/Listeners/CheckPasswordWasForceChanged.php create mode 100644 database/migrations/2024_12_14_150842_add_force_password_change_to_users_table.php create mode 100644 tests/Unit/CreateAdminActionTest.php create mode 100644 tests/Unit/ForceChangePasswordActionTest.php diff --git a/app/Actions/CreateAdminAction.php b/app/Actions/CreateAdminAction.php new file mode 100644 index 00000000..f41c6c30 --- /dev/null +++ b/app/Actions/CreateAdminAction.php @@ -0,0 +1,27 @@ +where(["is_admin_school" => true])->firstOrFail(); + + $user = new User($adminData); + $user->password = Hash::make($adminData["password"]); + $user->school()->associate($school); + $user->save(); + $user->syncRoles("admin"); + event(new Registered($user)); + + return $user; + } +} diff --git a/app/Actions/ForceChangePasswordAction.php b/app/Actions/ForceChangePasswordAction.php new file mode 100644 index 00000000..9676ac67 --- /dev/null +++ b/app/Actions/ForceChangePasswordAction.php @@ -0,0 +1,16 @@ +force_password_change = true; + $user->save(); + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 10d15104..be9b03da 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -4,13 +4,13 @@ namespace App\Http\Controllers; +use App\Actions\CreateAdminAction; +use App\Actions\ForceChangePasswordAction; use App\Helpers\SortHelper; use App\Http\Requests\AdminRequest; use App\Http\Requests\Auth\RegisterAdminRequest; use App\Http\Resources\UserResource; -use App\Models\School; use App\Models\User; -use Illuminate\Auth\Events\Registered; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Hash; @@ -32,23 +32,14 @@ public function index(SortHelper $sorter): Response ]); } - public function store(RegisterAdminRequest $request): RedirectResponse + public function store(RegisterAdminRequest $request, ForceChangePasswordAction $forceChangePasswordAction, CreateAdminAction $createAdminAction): RedirectResponse { - $school = School::query()->where(["is_admin_school" => true])->firstOrFail(); - $userExists = User::query()->where("email", $request->email)->exists(); - - if (!$userExists) { - $user = new User($request->validated()); - $user->password = Hash::make($request->password); - $user->school()->associate($school); - $user->save(); - $user->syncRoles("admin"); - event(new Registered($user)); - } + $user = $createAdminAction->execute($request->validated()); + $forceChangePasswordAction->execute($user); return redirect() ->route("admin.admins.index") - ->with("success", "Administrator został utworzony pomyślnie."); + ->with("status", "Administrator został utworzony pomyślnie."); } public function update(AdminRequest $request, User $user): RedirectResponse @@ -69,7 +60,7 @@ public function update(AdminRequest $request, User $user): RedirectResponse return redirect() ->route("admin.admins.index") - ->with("success", "Administrator zaktualizowany pomyślnie."); + ->with("status", "Administrator zaktualizowany pomyślnie."); } public function destroy(User $user): RedirectResponse diff --git a/app/Http/Controllers/ProfileUserController.php b/app/Http/Controllers/ProfileUserController.php index 7a06f489..5f9e9da3 100644 --- a/app/Http/Controllers/ProfileUserController.php +++ b/app/Http/Controllers/ProfileUserController.php @@ -6,6 +6,7 @@ use App\Http\Requests\Auth\ProfileUserPasswordResetRequest; use App\Http\Resources\UserResource; +use Illuminate\Auth\Events\PasswordReset; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -34,6 +35,8 @@ public function update(ProfileUserPasswordResetRequest $request, Hasher $hasher) $user->password = $hasher->make($validated["password"]); $user->save(); + event(new PasswordReset($user)); + return redirect()->back() ->with("status", "Zaktualizowano hasło"); } diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php index 2344d877..21cecef5 100644 --- a/app/Http/Controllers/QuizQuestionController.php +++ b/app/Http/Controllers/QuizQuestionController.php @@ -42,6 +42,6 @@ public function clone(QuizCloneService $service, Question $question, Quiz $quiz) return redirect() ->back() - ->with("success", "Pytanie zostało skopiowane"); + ->with("status", "Pytanie zostało skopiowane"); } } diff --git a/app/Http/Middleware/EnsureUserPasswordWasChanged.php b/app/Http/Middleware/EnsureUserPasswordWasChanged.php new file mode 100644 index 00000000..d63e162e --- /dev/null +++ b/app/Http/Middleware/EnsureUserPasswordWasChanged.php @@ -0,0 +1,25 @@ +user(); + + if ($user->force_password_change) { + return redirect(route("profile")) + ->with("status", "Proszę zmienić hasło."); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Auth/RegisterAdminRequest.php b/app/Http/Requests/Auth/RegisterAdminRequest.php index 50d83e9b..fe02ac8a 100644 --- a/app/Http/Requests/Auth/RegisterAdminRequest.php +++ b/app/Http/Requests/Auth/RegisterAdminRequest.php @@ -16,7 +16,7 @@ public function authorize(): bool public function rules(): array { return [ - "email" => ["required", "string", "email:rfc,dns", "max:255"], + "email" => ["required", "string", "unique:users", "email:rfc,dns", "max:255"], "firstname" => ["required", "string", "max:255"], "surname" => ["required", "string", "max:255"], "password" => ["required", "string", "min:8", "max:255"], diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index ae230c78..7eb27785 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -21,6 +21,7 @@ public function toArray(Request $request): array "email" => $this->email, "school" => SchoolResource::make($this->school), "isAnonymized" => $this->is_anonymized, + "forcePasswordChange" => $this->force_password_change, "isAdmin" => $this->hasRole("admin"), "isSuperAdmin" => $this->hasRole("super_admin"), "createdAt" => $this->created_at, diff --git a/app/Listeners/CheckPasswordWasForceChanged.php b/app/Listeners/CheckPasswordWasForceChanged.php new file mode 100644 index 00000000..830129a2 --- /dev/null +++ b/app/Listeners/CheckPasswordWasForceChanged.php @@ -0,0 +1,19 @@ +user->force_password_change) { + $event->user->force_password_change = false; + $event->user->save(); + } + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 2c900651..c7f79e9a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -30,6 +31,7 @@ * @property Carbon $updated_at * @property School $school * @property boolean $is_anonymized + * @property boolean $force_password_change * @property Collection $userQuizzes * @property Collection $assignedQuizzes */ @@ -45,6 +47,7 @@ class User extends Authenticatable implements MustVerifyEmail, CanResetPassword "email", "school_id", "is_anonymized", + "force_password_change", ]; protected $hidden = [ "remember_token", @@ -70,7 +73,7 @@ public function userQuizzes(): HasMany return $this->hasMany(UserQuiz::class); } - public function assignedQuizzes() + public function assignedQuizzes(): BelongsToMany { return $this->belongsToMany(Quiz::class, "quiz_assignments"); } diff --git a/database/migrations/2024_12_14_150842_add_force_password_change_to_users_table.php b/database/migrations/2024_12_14_150842_add_force_password_change_to_users_table.php new file mode 100644 index 00000000..1e91621c --- /dev/null +++ b/database/migrations/2024_12_14_150842_add_force_password_change_to_users_table.php @@ -0,0 +1,23 @@ +boolean("force_password_change")->default(false); + }); + } + + public function down(): void + { + Schema::table("users", function (Blueprint $table): void { + $table->dropColumn("force_password_change"); + }); + } +}; diff --git a/resources/js/Types/User.d.ts b/resources/js/Types/User.d.ts index 5f12f648..5148fd6e 100644 --- a/resources/js/Types/User.d.ts +++ b/resources/js/Types/User.d.ts @@ -5,6 +5,7 @@ interface User { email: string school: School isAnonymized: boolean + forcePasswordChange: boolean isAdmin: boolean isSuperAdmin: boolean createdAt: string diff --git a/routes/web.php b/routes/web.php index eab23371..e0e9f45d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,7 @@ use App\Http\Controllers\UserQuestionController; use App\Http\Controllers\UserQuizController; use App\Http\Middleware\EnsureQuizIsNotAlreadyStarted; +use App\Http\Middleware\EnsureUserPasswordWasChanged; use App\Models\Answer; use App\Models\Question; use Illuminate\Support\Facades\Route; @@ -40,7 +41,7 @@ Route::get("/auth/password/reset/{token}", [PasswordResetLinkController::class, "resetCreate"])->name("password.reset"); Route::post("/auth/password/reset", [PasswordResetLinkController::class, "resetStore"])->name("password.update"); -Route::group(["prefix" => "admin", "middleware" => ["auth", "role:admin|super_admin"]], function (): void { +Route::group(["prefix" => "admin", "middleware" => ["auth", "role:admin|super_admin", EnsureUserPasswordWasChanged::class]], function (): void { Route::get("/quizzes", [QuizController::class, "index"])->name("admin.quizzes.index"); Route::get("/quizzes/{quiz}", [QuizController::class, "show"])->name("admin.quizzes.demo"); Route::post("/quizzes", [QuizController::class, "store"])->name("admin.quizzes.store"); @@ -94,7 +95,7 @@ }); }); -Route::middleware(["auth", "verified"])->group(function (): void { +Route::middleware(["auth", "verified", EnsureUserPasswordWasChanged::class])->group(function (): void { Route::post("/quizzes/{quiz}/assign", [QuizController::class, "assign"])->can("assign,quiz")->name("quizzes.assign"); Route::post("/quizzes/{quiz}/start", [QuizController::class, "createUserQuiz"])->middleware(EnsureQuizIsNotAlreadyStarted::class)->can("submit,quiz")->name("quizzes.start"); Route::get("/quizzes/{quiz}/ranking", [RankingController::class, "indexUser"])->can("viewUserRanking,quiz")->name("quizzes.ranking"); @@ -103,6 +104,6 @@ Route::get("/quizzes/{userQuiz}/result", [UserQuizController::class, "result"])->can("result,userQuiz")->name("userQuizzes.result"); Route::patch("/questions/{userQuestion}/{answer}", [UserQuestionController::class, "answer"])->can("answer,userQuestion,answer")->name("questions.answer"); Route::get("/dashboard", [ContestController::class, "create"])->name("dashboard"); - Route::get("/profile", [ProfileUserController::class, "create"])->name("profile"); - Route::patch("/profile/password", [ProfileUserController::class, "update"])->name("profile.password.update"); + Route::get("/profile", [ProfileUserController::class, "create"])->name("profile")->withoutMiddleware(EnsureUserPasswordWasChanged::class); + Route::patch("/profile/password", [ProfileUserController::class, "update"])->name("profile.password.update")->withoutMiddleware(EnsureUserPasswordWasChanged::class); }); diff --git a/tests/Feature/ResetPasswordTest.php b/tests/Feature/ResetPasswordTest.php index 3450f9d2..6450019b 100644 --- a/tests/Feature/ResetPasswordTest.php +++ b/tests/Feature/ResetPasswordTest.php @@ -176,4 +176,25 @@ public function testUserCannotAccessResetPassword(): void ->patch(route("profile.password.update")) ->assertRedirect("/auth/login"); } + + public function testChangingPasswordResetForcePasswordChange(): void + { + $user = User::factory()->create([ + "password" => Hash::make("current-password"), + "force_password_change" => true, + ]); + + $this->actingAs($user) + ->from("/profile") + ->patch(route("profile.password.update"), [ + "current_password" => "current-password", + "password" => "new-password", + "password_confirmation" => "new-password", + ]); + + $this->assertDatabaseHas("users", [ + "id" => $user->id, + "force_password_change" => false, + ]); + } } diff --git a/tests/Unit/CreateAdminActionTest.php b/tests/Unit/CreateAdminActionTest.php new file mode 100644 index 00000000..7b022645 --- /dev/null +++ b/tests/Unit/CreateAdminActionTest.php @@ -0,0 +1,43 @@ +adminSchool()->create(); + $this->action = new CreateAdminAction(); + } + + public function testCreateAdmin(): void + { + $user = $this->action->execute([ + "firstname" => "Admin Name", + "surname" => "Admin Surname", + "email" => "adminexample@admin.com", + "password" => "password", + ]); + + $this->assertDatabaseHas("users", [ + "id" => $user->id, + "firstname" => "Admin Name", + "surname" => "Admin Surname", + "email" => "adminexample@admin.com", + ]); + + $this->assertTrue($user->hasRole("admin")); + } +} diff --git a/tests/Unit/ForceChangePasswordActionTest.php b/tests/Unit/ForceChangePasswordActionTest.php new file mode 100644 index 00000000..2fa47103 --- /dev/null +++ b/tests/Unit/ForceChangePasswordActionTest.php @@ -0,0 +1,35 @@ +action = new ForceChangePasswordAction(); + } + + public function testActionSetQuizLocal(): void + { + $user = User::factory()->create(); + $this->action->execute($user); + + $this->assertDatabaseHas("users", [ + "id" => $user->id, + "force_password_change" => true, + ]); + } +}