- Implement DoctorPermissionsModal for managing doctor permissions in clinics. - Create usePermissions hook to handle user permissions context. - Add migration for clinic_doctor_permissions table with default permissions. - Develop ClinicDoctorPermissionController for handling permissions API. - Create ClinicDoctorPermission entity to manage permissions data. - Implement ClinicDoctorPermissionRepository for database interactions. - Add ClinicDoctorPermissionChecker for permission validation logic. - Write tests for clinic doctor permissions functionality.
185 lines
7.0 KiB
PHP
185 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Clinic;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Clinic\Entity\ClinicDoctorPermission;
|
|
use App\Clinic\Repository\ClinicDoctorPermissionRepository;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* Per-doctor permissions inside a clinic: owner-only management, deep-merge
|
|
* semantics, lazy provisioning for pre-existing members, and context exposure.
|
|
*/
|
|
class ClinicDoctorPermissionTest extends ApiTestCase
|
|
{
|
|
private function createClinicWithDoctor(): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
|
$clinic = new Clinic($owner);
|
|
$clinic->setName('کلینیک تست');
|
|
|
|
$docUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
|
$doctor = new Doctor($docUser, 'دکتر عضو');
|
|
$doctor->setMobileNumber($docUser->getMobileNumber());
|
|
|
|
$this->em->persist($doctor);
|
|
$clinic->getDoctors()->add($doctor);
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
return [$owner, $clinic, $doctor, $docUser];
|
|
}
|
|
|
|
private function permRepo(): ClinicDoctorPermissionRepository
|
|
{
|
|
return static::getContainer()->get(ClinicDoctorPermissionRepository::class);
|
|
}
|
|
|
|
public function testOwnerReadsLazilyProvisionedDefaults(): void
|
|
{
|
|
[$owner, $clinic, $doctor] = $this->createClinicWithDoctor();
|
|
|
|
$res = $this->authJson('GET', "/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$doctor->getUuid()}/permissions", $owner);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertTrue($res['data']['active']);
|
|
self::assertSame(
|
|
ClinicDoctorPermission::DEFAULT_PERMISSIONS['resources'],
|
|
$res['data']['permissions']['resources'],
|
|
'a member added before this feature gets defaults on first read',
|
|
);
|
|
}
|
|
|
|
public function testPatchOnlyTouchesProvidedKeys(): void
|
|
{
|
|
[$owner, $clinic, $doctor] = $this->createClinicWithDoctor();
|
|
|
|
$res = $this->authJson(
|
|
'PATCH',
|
|
"/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$doctor->getUuid()}/permissions",
|
|
$owner,
|
|
['permissions' => ['resources' => ['payments' => ['create' => true]]]],
|
|
);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
$resources = $res['data']['permissions']['resources'];
|
|
self::assertTrue($resources['payments']['create']);
|
|
self::assertFalse($resources['payments']['delete'], 'untouched actions keep their value');
|
|
self::assertTrue($resources['appointments']['view'], 'untouched resources keep their value');
|
|
}
|
|
|
|
public function testUnknownResourceAndActionAreIgnored(): void
|
|
{
|
|
[$owner, $clinic, $doctor] = $this->createClinicWithDoctor();
|
|
|
|
$res = $this->authJson(
|
|
'PATCH',
|
|
"/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$doctor->getUuid()}/permissions",
|
|
$owner,
|
|
['permissions' => ['resources' => ['bogus' => ['view' => true], 'payments' => ['fly' => true]]]],
|
|
);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertArrayNotHasKey('bogus', $res['data']['permissions']['resources']);
|
|
self::assertArrayNotHasKey('fly', $res['data']['permissions']['resources']['payments']);
|
|
}
|
|
|
|
public function testDeactivationRevokesEverything(): void
|
|
{
|
|
[$owner, $clinic, $doctor] = $this->createClinicWithDoctor();
|
|
|
|
$this->authJson(
|
|
'PATCH',
|
|
"/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$doctor->getUuid()}/permissions",
|
|
$owner,
|
|
['active' => false],
|
|
);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$perm = $this->permRepo()->findOneFor(
|
|
$this->em->getRepository(Clinic::class)->find($clinic->getId()),
|
|
$this->em->getRepository(Doctor::class)->find($doctor->getId()),
|
|
);
|
|
self::assertFalse($perm->isActive());
|
|
self::assertFalse($perm->can('appointments', 'view'), 'inactive membership grants nothing');
|
|
}
|
|
|
|
public function testMemberDoctorCannotEditOwnPermissions(): void
|
|
{
|
|
[, $clinic, $doctor, $docUser] = $this->createClinicWithDoctor();
|
|
|
|
$this->authJson(
|
|
'PATCH',
|
|
"/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$doctor->getUuid()}/permissions",
|
|
$docUser,
|
|
['permissions' => ['resources' => ['payments' => ['delete' => true]]]],
|
|
);
|
|
|
|
self::assertSame(403, $this->responseCode());
|
|
}
|
|
|
|
public function testDoctorOfAnotherClinicIsNotFound(): void
|
|
{
|
|
[$owner, $clinic] = $this->createClinicWithDoctor();
|
|
|
|
$strangerUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
|
$stranger = new Doctor($strangerUser, 'دکتر بیرونی');
|
|
$this->em->persist($stranger);
|
|
$this->em->flush();
|
|
|
|
$this->authJson('GET', "/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$stranger->getUuid()}/permissions", $owner);
|
|
|
|
self::assertSame(404, $this->responseCode());
|
|
}
|
|
|
|
public function testOwnerIsNeverRestrictedByPermissions(): void
|
|
{
|
|
[$owner, $clinic, $doctor] = $this->createClinicWithDoctor();
|
|
|
|
$checker = static::getContainer()->get(\App\Clinic\Security\ClinicDoctorPermissionChecker::class);
|
|
$perm = $this->permRepo()->getOrCreate($clinic, $doctor);
|
|
$perm->setActive(false);
|
|
$this->em->flush();
|
|
|
|
self::assertTrue($checker->can($owner, $clinic, 'clinic_info', 'update'));
|
|
}
|
|
|
|
public function testMemberContextCarriesPermissionsAndOwnPracticeDoesNot(): void
|
|
{
|
|
[, $clinic, $doctor, $docUser] = $this->createClinicWithDoctor();
|
|
|
|
$res = $this->authJson('GET', '/oauth/userinfo', $docUser);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$contexts = $res['data']['available_contexts'];
|
|
$personal = array_values(array_filter($contexts, fn($c) => $c['type'] === 'doctor'));
|
|
$member = array_values(array_filter($contexts, fn($c) => $c['type'] === 'clinic'));
|
|
|
|
self::assertNotEmpty($personal);
|
|
self::assertNotEmpty($member);
|
|
self::assertNull($personal[0]['permissions'] ?? null, 'own practice is unrestricted');
|
|
self::assertSame(
|
|
ClinicDoctorPermission::DEFAULT_PERMISSIONS['resources'],
|
|
$member[0]['permissions']['resources'],
|
|
);
|
|
}
|
|
|
|
public function testDetachingDoctorRemovesPermissionRow(): void
|
|
{
|
|
[$owner, $clinic, $doctor] = $this->createClinicWithDoctor();
|
|
$this->permRepo()->getOrCreate($clinic, $doctor);
|
|
|
|
$this->authJson('DELETE', "/api/v1/admin/clinic/{$clinic->getUuid()}/doctor/{$doctor->getUuid()}", $owner);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$reloadedClinic = $this->em->getRepository(Clinic::class)->find($clinic->getId());
|
|
$reloadedDoctor = $this->em->getRepository(Doctor::class)->find($doctor->getId());
|
|
self::assertNull($this->permRepo()->findOneFor($reloadedClinic, $reloadedDoctor));
|
|
}
|
|
}
|