New_Micro_Learning/backend/tests/Feature/Identity/InvitationAndPasswordTest.php

111 خطوط
4.1 KiB
PHP

<?php
namespace Tests\Feature\Identity;
use App\Models\User;
use App\Modules\Identity\Domain\Enums\UserRole;
use App\Modules\Identity\Domain\UserInvitation;
use App\Modules\Identity\Notifications\UserInvited;
use App\Modules\Subscriptions\Domain\Subscription;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class InvitationAndPasswordTest extends TestCase
{
use RefreshDatabase;
public function test_designer_can_invite_and_recipient_can_accept_without_token_leaking_from_api(): void
{
Notification::fake();
$designer = User::factory()->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);
}
}