create(['role' => UserRole::CourseDesigner]); Sanctum::actingAs($designer); $response = $this->postJson('/api/v1/user-invitations', [ 'email' => 'new.learner@example.test', 'role' => UserRole::Learner->value, ])->assertCreated()->assertJsonMissing(['token']); $token = null; Notification::assertSentOnDemand(UserInvited::class, function (UserInvited $notification) use (&$token) { $token = $notification->token; return true; }); $this->assertNotNull($token); $this->postJson('/api/v1/auth/invitations/accept', [ 'token' => $token, 'name' => 'New Learner', 'password' => 'a-secure-password', 'password_confirmation' => 'a-secure-password', ])->assertOk()->assertJsonStructure(['data' => ['token', 'userId']]); $user = User::query()->where('email', 'new.learner@example.test')->firstOrFail(); $this->assertSame($designer->organization_id, $user->organization_id); $this->assertSame(UserRole::Learner, $user->role); $this->assertNotNull(UserInvitation::query()->find($response->json('data.id'))->accepted_at); } public function test_seat_limit_blocks_new_invitation(): void { Notification::fake(); $designer = User::factory()->create(['role' => UserRole::CourseDesigner]); Subscription::query()->create([ 'organization_id' => $designer->organization_id, 'plan_key' => 'limited', 'status' => 'active', 'starts_at' => now()->subDay(), 'seat_limit' => 1, ]); Sanctum::actingAs($designer); $this->postJson('/api/v1/user-invitations', [ 'email' => 'over.limit@example.test', 'role' => UserRole::Learner->value, ])->assertUnprocessable()->assertJsonValidationErrors('email'); Notification::assertNothingSent(); } public function test_manager_cannot_invite_users(): void { $manager = User::factory()->create(['role' => UserRole::Manager]); Sanctum::actingAs($manager); $this->postJson('/api/v1/user-invitations', [ 'email' => 'learner@example.test', 'role' => UserRole::Learner->value, ])->assertForbidden(); } public function test_password_reset_changes_password_and_revokes_existing_tokens(): void { Notification::fake(); $user = User::factory()->create(['password' => 'old-password']); $user->createToken('existing'); $this->postJson('/api/v1/auth/forgot-password', ['email' => $user->email])->assertStatus(202); Notification::assertSentTo($user, ResetPassword::class); $token = Password::createToken($user); $this->postJson('/api/v1/auth/reset-password', [ 'email' => $user->email, 'token' => $token, 'password' => 'new-secure-password', 'password_confirmation' => 'new-secure-password', ])->assertOk()->assertJsonPath('data.reset', true); $this->assertTrue(Hash::check('new-secure-password', $user->fresh()->password)); $this->assertDatabaseCount('personal_access_tokens', 0); } public function test_forgot_password_does_not_reveal_unknown_email(): void { $this->postJson('/api/v1/auth/forgot-password', ['email' => 'unknown@example.test'])->assertStatus(202); } }