111 خطوط
4.1 KiB
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);
|
|
}
|
|
}
|