v1.0.0
This commit is contained in:
137
src/tests/Feature/OperatorTeamScopingTest.php
Normal file
137
src/tests/Feature/OperatorTeamScopingTest.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Operator\Queue;
|
||||
use App\Livewire\Operator\TicketShow;
|
||||
use App\Models\Team;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('a non-admin operator only sees their own team in the queue sidebar, not every team', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$operator = operatorUser('scoped-1@example.com');
|
||||
$myTeam = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
$otherTeam = Team::query()->create(['name' => 'Aplikacje']);
|
||||
$operator->teams()->attach($myTeam->id);
|
||||
|
||||
$teamKeys = Livewire::actingAs($operator)->test(Queue::class)
|
||||
->instance()->teams->pluck('name')->all();
|
||||
|
||||
expect($teamKeys)->toBe(['Infrastruktura']);
|
||||
});
|
||||
|
||||
test('an admin (even if also an operator) sees every team in the queue sidebar', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$admin = User::query()->create(['name' => 'Admin Op', 'email' => 'admin-op@example.com', 'roles' => ['operator', 'admin']]);
|
||||
Team::query()->create(['name' => 'Infrastruktura']);
|
||||
Team::query()->create(['name' => 'Aplikacje']);
|
||||
|
||||
$teamNames = Livewire::actingAs($admin)->test(Queue::class)
|
||||
->instance()->teams->pluck('name')->sort()->values()->all();
|
||||
|
||||
expect($teamNames)->toBe(['Aplikacje', 'Infrastruktura']);
|
||||
});
|
||||
|
||||
test('a non-admin operator only sees tickets from their own team, unrouted tickets, or ones assigned to them', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$operator = operatorUser('scoped-2@example.com');
|
||||
$myTeam = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
$otherTeam = Team::query()->create(['name' => 'Aplikacje']);
|
||||
$operator->teams()->attach($myTeam->id);
|
||||
|
||||
$mine = makeTicket(['number' => '1001', 'team_id' => $myTeam->id]);
|
||||
$others = makeTicket(['number' => '1002', 'team_id' => $otherTeam->id]);
|
||||
$unrouted = makeTicket(['number' => '1003', 'team_id' => null]);
|
||||
$assignedToMeElsewhere = makeTicket(['number' => '1004', 'team_id' => $otherTeam->id, 'assignee_id' => $operator->id]);
|
||||
|
||||
Livewire::actingAs($operator)->test(Queue::class)
|
||||
->assertSee($mine->number)
|
||||
->assertSee($unrouted->number)
|
||||
->assertSee($assignedToMeElsewhere->number)
|
||||
->assertDontSee($others->number);
|
||||
});
|
||||
|
||||
test('an admin operator still sees tickets from every team', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$admin = User::query()->create(['name' => 'Admin Op', 'email' => 'admin-op-2@example.com', 'roles' => ['operator', 'admin']]);
|
||||
$teamA = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
$teamB = Team::query()->create(['name' => 'Aplikacje']);
|
||||
|
||||
$ticketA = makeTicket(['number' => '2001', 'team_id' => $teamA->id]);
|
||||
$ticketB = makeTicket(['number' => '2002', 'team_id' => $teamB->id]);
|
||||
|
||||
Livewire::actingAs($admin)->test(Queue::class)
|
||||
->assertSee($ticketA->number)
|
||||
->assertSee($ticketB->number);
|
||||
});
|
||||
|
||||
test('a non-admin operator gets a 403 opening a ticket outside their scope directly', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$operator = operatorUser('scoped-3@example.com');
|
||||
$myTeam = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
$otherTeam = Team::query()->create(['name' => 'Aplikacje']);
|
||||
$operator->teams()->attach($myTeam->id);
|
||||
|
||||
$outOfScope = makeTicket(['number' => '3001', 'team_id' => $otherTeam->id]);
|
||||
|
||||
Livewire::actingAs($operator)->test(TicketShow::class, ['ticket' => $outOfScope])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('a non-admin operator can still open a ticket outside their team if it is personally assigned to them', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$operator = operatorUser('scoped-4@example.com');
|
||||
$myTeam = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
$otherTeam = Team::query()->create(['name' => 'Aplikacje']);
|
||||
$operator->teams()->attach($myTeam->id);
|
||||
|
||||
$assignedElsewhere = makeTicket(['number' => '4001', 'team_id' => $otherTeam->id, 'assignee_id' => $operator->id]);
|
||||
|
||||
Livewire::actingAs($operator)->test(TicketShow::class, ['ticket' => $assignedElsewhere])
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('the team reassignment dropdown on a ticket only offers a non-admin operator their own teams', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$operator = operatorUser('scoped-5@example.com');
|
||||
$myTeam = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
Team::query()->create(['name' => 'Aplikacje']);
|
||||
$operator->teams()->attach($myTeam->id);
|
||||
|
||||
$ticket = makeTicket(['number' => '5001', 'team_id' => $myTeam->id]);
|
||||
|
||||
$teamNames = Livewire::actingAs($operator)->test(TicketShow::class, ['ticket' => $ticket])
|
||||
->instance()->teams->pluck('name')->all();
|
||||
|
||||
expect($teamNames)->toBe(['Infrastruktura']);
|
||||
});
|
||||
|
||||
test('merging cannot pull in a ticket outside the operators scope via a crafted selection', function () {
|
||||
seedStatusesAndPriorities();
|
||||
$operator = operatorUser('scoped-6@example.com');
|
||||
$myTeam = Team::query()->create(['name' => 'Infrastruktura']);
|
||||
$otherTeam = Team::query()->create(['name' => 'Aplikacje']);
|
||||
$operator->teams()->attach($myTeam->id);
|
||||
|
||||
$mineA = makeTicket(['number' => '6001', 'team_id' => $myTeam->id]);
|
||||
$mineB = makeTicket(['number' => '6002', 'team_id' => $myTeam->id]);
|
||||
$outOfScope = makeTicket(['number' => '6003', 'team_id' => $otherTeam->id]);
|
||||
|
||||
Livewire::actingAs($operator)->test(Queue::class)
|
||||
->call('toggleSelect', $mineA->id)
|
||||
->call('toggleSelect', $outOfScope->id)
|
||||
->call('mergeSelected'); // only 1 ticket is actually in scope, so this must be a no-op
|
||||
|
||||
expect(Ticket::query()->find($outOfScope->id)->status_key)->not->toBe('closed');
|
||||
|
||||
Livewire::actingAs($operator)->test(Queue::class)
|
||||
->call('toggleSelect', $mineA->id)
|
||||
->call('toggleSelect', $mineB->id)
|
||||
->call('toggleSelect', $outOfScope->id)
|
||||
->call('requestDeleteSelected')
|
||||
->call('confirmDeleteSelected');
|
||||
|
||||
expect(Ticket::query()->find($mineA->id))->toBeNull()
|
||||
->and(Ticket::query()->find($mineB->id))->toBeNull()
|
||||
->and(Ticket::query()->find($outOfScope->id))->not->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user