150 خطوط
7.3 KiB
PHP
150 خطوط
7.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Teams;
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Identity\Domain\Enums\UserRole;
|
|
use App\Modules\Teams\Domain\Team;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Laravel\Sanctum\Sanctum;
|
|
use Tests\TestCase;
|
|
|
|
class TeamAuthorizationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_manager_only_sees_members_of_managed_teams(): void
|
|
{
|
|
$manager = User::factory()->create(['role' => UserRole::Manager]);
|
|
$otherManager = User::factory()->for($manager->organization)->create(['role' => UserRole::Manager]);
|
|
$ownLearner = User::factory()->for($manager->organization)->create(['role' => UserRole::Learner]);
|
|
$privateLearner = User::factory()->for($manager->organization)->create(['role' => UserRole::Learner]);
|
|
$ownTeam = $this->team($manager, 'Own team');
|
|
$privateTeam = $this->team($manager, 'Private team');
|
|
$ownTeam->managers()->attach($manager);
|
|
$ownTeam->members()->attach($ownLearner);
|
|
$privateTeam->managers()->attach($otherManager);
|
|
$privateTeam->members()->attach($privateLearner);
|
|
Sanctum::actingAs($manager);
|
|
|
|
$response = $this->getJson('/api/v1/users')->assertOk();
|
|
$response->assertJsonFragment(['id' => $ownLearner->getKey()]);
|
|
$response->assertJsonMissing(['id' => $privateLearner->getKey()]);
|
|
|
|
$teams = $this->getJson('/api/v1/teams')->assertOk();
|
|
$teams->assertJsonFragment(['id' => $ownTeam->getKey()]);
|
|
$teams->assertJsonMissing(['id' => $privateTeam->getKey()]);
|
|
}
|
|
|
|
public function test_designer_can_create_team_and_attach_same_tenant_member_and_manager(): void
|
|
{
|
|
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
|
|
$manager = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager]);
|
|
$learner = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner]);
|
|
Sanctum::actingAs($designer);
|
|
|
|
$teamId = $this->postJson('/api/v1/teams', ['name' => 'Sales'])->assertCreated()->json('data.id');
|
|
$this->putJson('/api/v1/teams/'.$teamId.'/members', ['userId' => $learner->getKey()])->assertOk();
|
|
$this->putJson('/api/v1/teams/'.$teamId.'/managers', ['userId' => $manager->getKey()])->assertOk();
|
|
|
|
$this->assertDatabaseHas('team_memberships', ['team_id' => $teamId, 'user_id' => $learner->getKey()]);
|
|
$this->assertDatabaseHas('team_managers', ['team_id' => $teamId, 'user_id' => $manager->getKey()]);
|
|
}
|
|
|
|
public function test_selecting_manager_automatically_attaches_active_direct_reports(): void
|
|
{
|
|
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
|
|
$manager = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager]);
|
|
$report = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner, 'direct_manager_id' => $manager->getKey()]);
|
|
$other = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner]);
|
|
Sanctum::actingAs($designer);
|
|
|
|
$teamId = $this->postJson('/api/v1/teams', ['name' => 'Automatic', 'managerId' => $manager->getKey()])
|
|
->assertCreated()
|
|
->assertJsonPath('data.managerCount', 1)
|
|
->assertJsonPath('data.memberCount', 1)
|
|
->json('data.id');
|
|
|
|
$this->assertDatabaseHas('team_managers', ['team_id' => $teamId, 'user_id' => $manager->getKey()]);
|
|
$this->assertDatabaseHas('team_memberships', ['team_id' => $teamId, 'user_id' => $report->getKey()]);
|
|
$this->assertDatabaseMissing('team_memberships', ['team_id' => $teamId, 'user_id' => $other->getKey()]);
|
|
}
|
|
|
|
public function test_manager_cannot_mutate_team_membership(): void
|
|
{
|
|
$manager = User::factory()->create(['role' => UserRole::Manager]);
|
|
$learner = User::factory()->for($manager->organization)->create();
|
|
$team = $this->team($manager, 'Managed team');
|
|
$team->managers()->attach($manager);
|
|
Sanctum::actingAs($manager);
|
|
|
|
$this->putJson('/api/v1/teams/'.$team->getKey().'/members', ['userId' => $learner->getKey()])->assertForbidden();
|
|
}
|
|
|
|
public function test_cross_tenant_user_cannot_be_attached_to_team(): void
|
|
{
|
|
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
|
|
$otherUser = User::factory()->create();
|
|
$team = $this->team($designer, 'Local team');
|
|
Sanctum::actingAs($designer);
|
|
|
|
$this->putJson('/api/v1/teams/'.$team->getKey().'/members', ['userId' => $otherUser->getKey()])->assertNotFound();
|
|
}
|
|
|
|
public function test_designer_can_read_update_and_detach_team_assignments(): void
|
|
{
|
|
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
|
|
$manager = User::factory()->for($designer->organization)->create(['role' => UserRole::Manager]);
|
|
$learner = User::factory()->for($designer->organization)->create(['role' => UserRole::Learner]);
|
|
$team = $this->team($designer, 'Support');
|
|
$team->managers()->attach($manager);
|
|
$team->members()->attach($learner);
|
|
Sanctum::actingAs($designer);
|
|
|
|
$this->getJson('/api/v1/teams/'.$team->getKey())
|
|
->assertOk()
|
|
->assertJsonPath('data.memberCount', 1)
|
|
->assertJsonPath('data.managerCount', 1)
|
|
->assertJsonPath('data.members.0.id', $learner->getKey())
|
|
->assertJsonMissing(['pivot']);
|
|
$this->patchJson('/api/v1/teams/'.$team->getKey(), ['name' => 'Support', 'description' => 'Updated'])
|
|
->assertOk()
|
|
->assertJsonPath('data.description', 'Updated');
|
|
$this->deleteJson('/api/v1/teams/'.$team->getKey().'/members/'.$learner->getKey())->assertOk();
|
|
$this->deleteJson('/api/v1/teams/'.$team->getKey().'/managers/'.$manager->getKey())->assertOk();
|
|
$this->assertDatabaseMissing('team_memberships', ['team_id' => $team->getKey(), 'user_id' => $learner->getKey()]);
|
|
$this->assertDatabaseMissing('team_managers', ['team_id' => $team->getKey(), 'user_id' => $manager->getKey()]);
|
|
}
|
|
|
|
public function test_manager_cannot_read_an_unmanaged_team_detail(): void
|
|
{
|
|
$manager = User::factory()->create(['role' => UserRole::Manager]);
|
|
$unmanaged = $this->team($manager, 'Unmanaged');
|
|
Sanctum::actingAs($manager);
|
|
|
|
$this->getJson('/api/v1/teams/'.$unmanaged->getKey())->assertNotFound();
|
|
}
|
|
|
|
public function test_cross_tenant_team_and_detach_targets_are_not_disclosed(): void
|
|
{
|
|
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
|
|
$foreignDesigner = User::factory()->create(['role' => UserRole::CourseDesigner]);
|
|
$foreignTeam = $this->team($foreignDesigner, 'Foreign');
|
|
$localTeam = $this->team($designer, 'Local');
|
|
Sanctum::actingAs($designer);
|
|
|
|
$this->getJson('/api/v1/teams/'.$foreignTeam->getKey())->assertNotFound();
|
|
$this->deleteJson('/api/v1/teams/'.$localTeam->getKey().'/members/'.$foreignDesigner->getKey())->assertNotFound();
|
|
}
|
|
|
|
private function team(User $creator, string $name): Team
|
|
{
|
|
return Team::query()->create([
|
|
'organization_id' => $creator->organization_id,
|
|
'name' => $name,
|
|
'status' => 'active',
|
|
'created_by' => $creator->getKey(),
|
|
]);
|
|
}
|
|
}
|