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']); // ترتیب و مجموعهٔ کلیدها را PermissionCatalog تعیین می‌کند، نه ترتیبِ // نوشتنِ DEFAULT_PERMISSIONS؛ مقادیر همان پیش‌فرضِ نقش می‌مانند. self::assertSame( PermissionCatalog::merge([], 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( PermissionCatalog::merge([], 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)); } }