138 خطوط
5.7 KiB
PHP
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;
|
|
}
|
|
}
|