CRM/backend/tests/Feature/TaskAuthorizationLifecycleT...

138 خطوط
5.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Lead;
use App\Models\Task;
use App\Models\Team;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TaskAuthorizationLifecycleTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
}
public function test_task_lists_and_direct_access_are_scoped_by_role_and_team(): void
{
$admin = $this->user('admin');
[$supervisor, $agent] = $this->teamUsers('Own');
[$otherSupervisor, $otherAgent] = $this->teamUsers('Other');
$own = $this->task($supervisor, $agent, 'Own team task', 'team');
$other = $this->task($otherSupervisor, $otherAgent, 'Other team task', 'team');
$this->actingAs($admin)->getJson('/api/tasks')->assertOk()->assertJsonPath('meta.total', 2);
$this->actingAs($supervisor)->getJson('/api/tasks')->assertOk()->assertJsonPath('meta.total', 1)->assertJsonPath('data.0.id', $own->id);
$this->actingAs($agent)->getJson('/api/tasks')->assertOk()->assertJsonPath('meta.total', 1)->assertJsonPath('data.0.id', $own->id);
$this->actingAs($agent)->getJson("/api/tasks/{$other->id}")->assertForbidden();
}
public function test_cross_team_inactive_assignee_and_unauthorized_entity_are_rejected(): void
{
[$supervisor, $agent] = $this->teamUsers('Own');
[, $otherAgent] = $this->teamUsers('Other');
$task = $this->task($supervisor, $agent, 'Scoped task', 'team');
$this->actingAs($supervisor)->postJson("/api/tasks/{$task->id}/assign", [
'assigned_to' => $otherAgent->id,
'version' => 1,
])->assertForbidden();
$inactive = $this->user('agent', ['is_active' => false]);
$this->actingAs($supervisor)->postJson("/api/tasks/{$task->id}/assign", [
'assigned_to' => $inactive->id,
'version' => 1,
])->assertUnprocessable()->assertJsonPath('code', 'VALIDATION_FAILED');
$foreignLead = Lead::create([
'first_name' => 'Other', 'last_name' => 'Lead', 'company' => 'Foreign',
'phone' => '02112345678', 'assigned_to' => $otherAgent->id,
]);
$this->actingAs($agent)->postJson('/api/tasks', [
'subject' => 'Unauthorized relation',
'taskable_type' => 'lead',
'taskable_id' => $foreignLead->id,
])->assertForbidden();
}
public function test_task_lifecycle_optimistic_lock_and_transactional_bulk_actions(): void
{
[$supervisor, $agent] = $this->teamUsers('Lifecycle');
$created = $this->actingAs($supervisor)->postJson('/api/tasks', [
'subject' => 'Call customer',
'assigned_to' => $agent->id,
'priority' => 'high',
'visibility' => 'team',
'due_at' => now()->addDay()->toISOString(),
])->assertCreated()->assertJsonPath('data.status', 'open');
$taskId = $created->json('data.id');
$started = $this->actingAs($agent)->postJson("/api/tasks/{$taskId}/start", ['version' => 1])
->assertOk()->assertJsonPath('data.status', 'in_progress');
$this->actingAs($agent)->patchJson("/api/tasks/{$taskId}", [
'subject' => 'Stale edit',
'version' => 1,
])->assertForbidden();
$this->actingAs($supervisor)->patchJson("/api/tasks/{$taskId}", [
'subject' => 'Stale creator edit',
'version' => 1,
])->assertStatus(409)->assertJsonPath('code', 'VERSION_CONFLICT');
$completed = $this->actingAs($agent)->postJson("/api/tasks/{$taskId}/complete", ['version' => $started->json('data.version')])
->assertOk()->assertJsonPath('data.status', 'done');
$this->assertNotNull(Task::find($taskId)->completed_at);
$reopened = $this->actingAs($agent)->postJson("/api/tasks/{$taskId}/reopen", ['version' => $completed->json('data.version')])
->assertOk()->assertJsonPath('data.status', 'open');
$this->assertNull(Task::find($taskId)->completed_at);
$second = $this->task($supervisor, $agent, 'Second', 'team');
$this->actingAs($supervisor)->postJson('/api/tasks/bulk-complete', [
'task_ids' => [$taskId, $second->id],
])->assertOk()->assertJsonCount(2, 'data');
$this->assertSame(2, Task::whereIn('id', [$taskId, $second->id])->where('status', 'done')->count());
$this->assertDatabaseHas('activity_logs', ['action' => 'task_bulk_completed', 'subject_id' => $second->id]);
$this->assertDatabaseHas('internal_notifications', ['user_id' => $agent->id, 'type' => 'task_assigned']);
}
private function task(User $creator, User $assignee, string $subject, string $visibility): Task
{
return Task::create([
'subject' => $subject,
'assigned_to' => $assignee->id,
'assigned_by' => $creator->id,
'created_by' => $creator->id,
'priority' => 'normal',
'status' => 'open',
'visibility' => $visibility,
]);
}
private function teamUsers(string $name): array
{
$supervisor = $this->user('supervisor');
$agent = $this->user('agent');
$team = Team::create(['name' => $name, 'supervisor_id' => $supervisor->id, 'is_active' => true]);
$team->members()->attach([$supervisor->id, $agent->id]);
return [$supervisor, $agent];
}
private function user(string $role, array $attributes = []): User
{
$user = User::factory()->create(array_merge(['is_active' => true], $attributes));
$user->assignRole($role);
return $user;
}
}