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

160 خطوط
7.7 KiB
PHP

<?php
namespace Tests\Feature\Identity;
use App\Models\User;
use App\Modules\Identity\Domain\Enums\AccountStatus;
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\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class UserManagementTest extends TestCase
{
use RefreshDatabase;
public function test_designer_can_search_and_filter_safe_tenant_user_directory(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$match = User::factory()->for($designer->organization)->create([
'name' => 'Sara Manager',
'role' => UserRole::Manager,
'status' => AccountStatus::Active,
]);
User::factory()->for($designer->organization)->create(['name' => 'Other Learner', 'role' => UserRole::Learner]);
$foreign = User::factory()->create(['name' => 'Sara Manager', 'role' => UserRole::Manager]);
Sanctum::actingAs($designer);
$response = $this->getJson('/api/v1/users?search=Sara&role=manager&status=active&perPage=10')
->assertOk()
->assertJsonPath('meta.total', 1)
->assertJsonPath('data.0.id', $match->getKey())
->assertJsonMissing(['id' => $foreign->getKey()]);
$this->assertArrayNotHasKey('password', $response->json('data.0'));
$this->assertArrayNotHasKey('remember_token', $response->json('data.0'));
}
public function test_designer_can_update_same_tenant_user_and_disabling_revokes_tokens(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$target = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner]);
$target->createToken('existing');
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/users/'.$target->getKey(), [
'role' => UserRole::Manager->value,
'status' => AccountStatus::Disabled->value,
])->assertOk()
->assertJsonPath('data.role', UserRole::Manager->value)
->assertJsonPath('data.status', AccountStatus::Disabled->value);
$this->assertDatabaseMissing('personal_access_tokens', ['tokenable_id' => $target->getKey()]);
}
public function test_designer_can_edit_workforce_profile_and_download_real_xlsx_template(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$manager = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager, 'job_level' => 'manager']);
$target = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner]);
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/users/'.$target->getKey(), [
'firstName' => 'مریم', 'lastName' => 'احمدی', 'email' => 'maryam.updated@example.test',
'department' => 'عملیات', 'jobLevel' => 'specialist', 'directManagerId' => $manager->getKey(),
])->assertOk()
->assertJsonPath('data.name', 'مریم احمدی')
->assertJsonPath('data.email', 'maryam.updated@example.test')
->assertJsonPath('data.directManager.id', $manager->getKey());
$this->assertDatabaseHas('users', ['id' => $target->getKey(), 'department' => 'عملیات', 'direct_manager_id' => $manager->getKey()]);
$template = $this->get('/api/v1/users/import-template')->assertOk()
->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$this->assertStringStartsWith('PK', $template->streamedContent());
}
public function test_workforce_edit_rejects_self_manager_and_hierarchy_cycles(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$first = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager, 'job_level' => 'manager']);
$second = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager, 'job_level' => 'manager', 'direct_manager_id' => $first->getKey()]);
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/users/'.$first->getKey(), ['directManagerId' => $first->getKey()])
->assertUnprocessable()->assertJsonValidationErrors('directManagerId');
$this->patchJson('/api/v1/users/'.$first->getKey(), ['directManagerId' => $second->getKey()])
->assertUnprocessable()->assertJsonValidationErrors('directManagerId');
}
public function test_last_active_designer_cannot_be_disabled_or_demoted(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/users/'.$designer->getKey(), ['status' => AccountStatus::Disabled->value])
->assertUnprocessable()
->assertJsonValidationErrors('role');
$this->patchJson('/api/v1/users/'.$designer->getKey(), ['role' => UserRole::Manager->value])
->assertUnprocessable()
->assertJsonValidationErrors('role');
}
public function test_user_mutation_is_tenant_scoped_and_manager_is_read_only(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$foreign = User::factory()->create();
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/users/'.$foreign->getKey(), ['status' => AccountStatus::Disabled->value])->assertNotFound();
$manager = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager]);
Sanctum::actingAs($manager);
$this->patchJson('/api/v1/users/'.$designer->getKey(), ['status' => AccountStatus::Disabled->value])->assertForbidden();
}
public function test_seat_limit_blocks_reactivation_of_a_disabled_user(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$disabled = User::factory()->for($designer->organization)->create(['status' => AccountStatus::Disabled]);
Subscription::query()->create([
'organization_id' => $designer->organization_id,
'plan_key' => 'limited',
'status' => 'active',
'starts_at' => now()->subDay(),
'seat_limit' => 1,
]);
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/users/'.$disabled->getKey(), ['status' => AccountStatus::Active->value])
->assertUnprocessable()
->assertJsonValidationErrors('status');
}
public function test_designer_can_list_resend_and_revoke_invitation_without_token_leakage(): void
{
Notification::fake();
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$id = $this->postJson('/api/v1/user-invitations', [
'email' => 'phase3@example.test',
'role' => UserRole::Learner->value,
])->assertCreated()->json('data.id');
$this->getJson('/api/v1/user-invitations')
->assertOk()
->assertJsonPath('data.0.status', 'pending')
->assertJsonMissing(['token_hash']);
$this->postJson('/api/v1/user-invitations/'.$id.'/resend')
->assertOk()
->assertJsonPath('data.status', 'pending')
->assertJsonMissing(['token']);
$this->deleteJson('/api/v1/user-invitations/'.$id)->assertOk()->assertJsonPath('data.revoked', true);
$this->assertNotNull(UserInvitation::query()->findOrFail($id)->revoked_at);
Notification::assertSentOnDemandTimes(UserInvited::class, 2);
}
}