Files
clinicpro/tests/Clinic/ClinicDoctorPermissionTest.php
T
hamed e4ddd38f0c feat: add per-doctor permissions management in clinics
- 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.
2026-07-18 09:44:13 +03:30

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));
}
}