createUser(['ROLE_CLINIC']); $clinic = new Clinic($owner); $this->em->persist($clinic); $doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر تست'); $this->em->persist($doctor); $clinic->getDoctors()->add($doctor); $secretary = $this->createUser(['ROLE_SECRETARY']); $rel = new DoctorSecretary($doctor, $secretary, DoctorSecretary::OWNER_CLINIC, $clinic); $this->em->persist($rel); $this->em->persist(new UserActiveContext($secretary, $clinic->getUuid())); return [$secretary, $rel]; } public function testInventoryDeniedByDefault(): void { // DEFAULT_PERMISSIONS: inventory.* = false [$secretary] = $this->makeClinicSecretary(); $this->em->flush(); $this->authJson('GET', '/api/v1/inventory-items', $secretary); $this->assertSame(403, $this->responseCode()); } public function testInventoryAllowedWhenGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['inventory' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', '/api/v1/inventory-items', $secretary); $this->assertSame(200, $this->responseCode()); } public function testPatientCreateDeniedByDefault(): void { // DEFAULT_PERMISSIONS: patients.create = false (view is true) [$secretary] = $this->makeClinicSecretary(); $this->em->flush(); $this->authJson('POST', '/api/v1/patient', $secretary, ['name' => 'x']); $this->assertSame(403, $this->responseCode()); } public function testInventoryCreateDeniedButViewGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['inventory' => ['view' => true, 'create' => false]]]); $this->em->flush(); // مشاهده مجاز $this->authJson('GET', '/api/v1/inventory-items', $secretary); $this->assertSame(200, $this->responseCode()); // ایجاد ممنوع $this->authJson('POST', '/api/v1/inventory-item', $secretary, ['name' => 'گاز استریل']); $this->assertSame(403, $this->responseCode()); } // service-items (listAllItems) فقط توگلِ permission را می‌سنجد — برخلاف // service-sections که پیش از آن، گیتِ اشتراک (assertServicesGate) هم دارد. public function testServicesDeniedByDefault(): void { // DEFAULT_PERMISSIONS: services.* = false [$secretary] = $this->makeClinicSecretary(); $this->em->flush(); $this->authJson('GET', '/api/v1/service-items', $secretary); $this->assertSame(403, $this->responseCode()); } public function testServicesAllowedWhenGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['services' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', '/api/v1/service-items', $secretary); $this->assertSame(200, $this->responseCode()); } public function testServicesCreateDeniedButViewGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['services' => ['view' => true, 'create' => false]]]); $this->em->flush(); // مشاهده مجاز $this->authJson('GET', '/api/v1/service-items', $secretary); $this->assertSame(200, $this->responseCode()); // ایجاد ممنوع — گیتِ permission پیش از گیتِ اشتراک اجرا می‌شود. $this->authJson('POST', '/api/v1/service-section', $secretary, ['name' => 'بخش تست']); $this->assertSame(403, $this->responseCode()); } // ── Phase B resources ───────────────────────────────────────────────────── public function testStaffDeniedByDefault(): void { [$secretary] = $this->makeClinicSecretary(); $this->em->flush(); $this->authJson('GET', '/api/v1/staff', $secretary); $this->assertSame(403, $this->responseCode()); } public function testStaffAllowedWhenGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['staff' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', '/api/v1/staff', $secretary); $this->assertSame(200, $this->responseCode()); } public function testStaffCreateDeniedButViewGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['staff' => ['view' => true, 'create' => false]]]); $this->em->flush(); $this->authJson('GET', '/api/v1/staff', $secretary); $this->assertSame(200, $this->responseCode()); $this->authJson('POST', '/api/v1/staff', $secretary, ['full_name' => 'خانم تست']); $this->assertSame(403, $this->responseCode()); } public function testDiscountsDeniedByDefault(): void { [$secretary] = $this->makeClinicSecretary(); $this->em->flush(); $this->authJson('GET', '/api/v1/admin/discount-rules', $secretary); $this->assertSame(403, $this->responseCode()); } public function testDiscountsAllowedWhenGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['discounts' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', '/api/v1/admin/discount-rules', $secretary); $this->assertSame(200, $this->responseCode()); } public function testSmsDeniedByDefault(): void { [$secretary] = $this->makeClinicSecretary(); $this->em->flush(); $this->authJson('GET', '/api/v1/sms/wallet/balance', $secretary); $this->assertSame(403, $this->responseCode()); } public function testSmsAllowedWhenGranted(): void { [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['sms' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', '/api/v1/sms/wallet/balance', $secretary); $this->assertSame(200, $this->responseCode()); } public function testClinicDoctorsDeniedByDefault(): void { [$secretary, , $clinic] = $this->makeClinicSecretaryWithClinic(); $this->em->flush(); $this->authJson('GET', "/api/v1/admin/clinic/{$clinic->getUuid()}/doctor-permissions", $secretary); $this->assertSame(403, $this->responseCode()); } public function testClinicDoctorsAllowedWhenGranted(): void { [$secretary, $rel, $clinic] = $this->makeClinicSecretaryWithClinic(); $rel->mergePermissions(['resources' => ['clinic_doctors' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', "/api/v1/admin/clinic/{$clinic->getUuid()}/doctor-permissions", $secretary); $this->assertSame(200, $this->responseCode()); } public function testAppointmentSettingsDeniedByDefault(): void { [$secretary, , $clinic, $doctor] = $this->makeClinicSecretaryWithClinic(); $this->em->flush(); // clinic_uuid لازم است تا محیطِ کلینیک حل شود (مثل پزشکِ عضو کلینیک). $this->authJson('GET', "/api/v1/appointment-settings/holidays/list/{$doctor->getUuid()}?clinic_uuid={$clinic->getUuid()}", $secretary); $this->assertSame(403, $this->responseCode()); } public function testAppointmentSettingsAllowedWhenGranted(): void { [$secretary, $rel, $clinic, $doctor] = $this->makeClinicSecretaryWithClinic(); $rel->mergePermissions(['resources' => ['appointment_settings' => ['view' => true]]]); $this->em->flush(); $this->authJson('GET', "/api/v1/appointment-settings/holidays/list/{$doctor->getUuid()}?clinic_uuid={$clinic->getUuid()}", $secretary); $this->assertSame(200, $this->responseCode()); } public function testPatientDeleteSeparateFromUpdate(): void { // منشی با patients.update ولی بدون patients.delete نباید بتواند حذف کند. // گیتِ delete پیش از واکشیِ رکورد اجرا می‌شود، پس uuidِ ناموجود هم ۴۰۳ می‌دهد. [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['patients' => ['view' => true, 'update' => true, 'delete' => false]]]); $this->em->flush(); $this->authJson('DELETE', '/api/v1/patient/note/00000000-0000-0000-0000-000000000000', $secretary); $this->assertSame(403, $this->responseCode(), 'حذف باید جدا از ویرایش کنترل شود'); } public function testPatientDeleteAllowedWhenGranted(): void { // با patients.delete، گیت عبور می‌کند و به «یافت نشد» می‌رسد (نه ۴۰۳). [$secretary, $rel] = $this->makeClinicSecretary(); $rel->mergePermissions(['resources' => ['patients' => ['view' => true, 'delete' => true]]]); $this->em->flush(); $this->authJson('DELETE', '/api/v1/patient/note/00000000-0000-0000-0000-000000000000', $secretary); $this->assertSame(404, $this->responseCode()); } public function testDoctorListReturnsOnlyAssignedDoctors(): void { // کلینیک با دو پزشک؛ منشی فقط به یکی تخصیص داده شده. $owner = $this->createUser(['ROLE_CLINIC']); $clinic = new Clinic($owner); $this->em->persist($clinic); $assigned = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر تخصیص‌یافته'); $unassigned = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر دیگر'); $this->em->persist($assigned); $this->em->persist($unassigned); $clinic->getDoctors()->add($assigned); $clinic->getDoctors()->add($unassigned); $secretary = $this->createUser(['ROLE_SECRETARY']); $this->em->persist(new DoctorSecretary($assigned, $secretary, DoctorSecretary::OWNER_CLINIC, $clinic)); $this->em->persist(new UserActiveContext($secretary, $clinic->getUuid())); $this->em->flush(); // اندپوینتِ احرازشدهٔ پنل (نه /clinic/doctor-list که عمومی است). $body = $this->authJson('GET', '/api/v1/my/clinic-doctors', $secretary); $this->assertSame(200, $this->responseCode()); $names = array_map(static fn($d) => $d['name'], $body['data']['data']); $this->assertContains('دکتر تخصیص‌یافته', $names); $this->assertNotContains('دکتر دیگر', $names, 'منشی نباید پزشکِ تخصیص‌نیافته را ببیند'); } /** مثل makeClinicSecretary اما clinic و doctor را هم برمی‌گرداند. */ private function makeClinicSecretaryWithClinic(): array { $owner = $this->createUser(['ROLE_CLINIC']); $clinic = new Clinic($owner); $this->em->persist($clinic); $doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر تست'); $this->em->persist($doctor); $clinic->getDoctors()->add($doctor); $secretary = $this->createUser(['ROLE_SECRETARY']); $rel = new DoctorSecretary($doctor, $secretary, DoctorSecretary::OWNER_CLINIC, $clinic); $this->em->persist($rel); $this->em->persist(new UserActiveContext($secretary, $clinic->getUuid())); return [$secretary, $rel, $clinic, $doctor]; } }