createUser(['ROLE_DOCTOR']), 'دکتر ماتریس'); $this->em->persist($doctor); $this->em->flush(); return $doctor; } private function makeClinic(?User $owner = null): Clinic { $clinic = new Clinic($owner ?? $this->createUser(['ROLE_CLINIC'])); $clinic->setName('کلینیک ماتریس'); $this->em->persist($clinic); $this->em->flush(); return $clinic; } /** عضویت به‌علاوهٔ مجوز discounts.view که در DEFAULT_PERMISSIONS خاموش است. */ private function joinClinic(Clinic $clinic, Doctor $doctor): void { $clinic->getDoctors()->add($doctor); $permission = new ClinicDoctorPermission($clinic, $doctor); $permission->mergePermissions(['resources' => ['discounts' => ['view' => true]]]); $this->em->persist($permission); $this->em->flush(); } private function setClinicContext(User $user, Clinic $clinic): void { $this->em->persist(new UserActiveContext($user, $clinic->getUuid(), EntityContext::TYPE_CLINIC)); $this->em->flush(); } private function rule(string $entityType, int $entityId, string $name): DiscountRule { $rule = new DiscountRule($entityType, $entityId, $name, DiscountRule::TYPE_INVOICE_AMOUNT); $this->em->persist($rule); $this->em->flush(); return $rule; } /** * پاسخ این endpoint با success(['data' => …]) ساخته می‌شود، پس یک لایه * تودرتوی اضافه دارد — همان دام double-nesting که در CLAUDE.md ثبت شده. * * @return string[] */ private function listedNames(User $user): array { $res = $this->authJson('GET', self::LIST_URL, $user); self::assertSame(200, $this->responseCode()); return array_column($res['data']['data'] ?? [], 'name'); } public function testIndependentDoctorSeesOnlyItsOwnRules(): void { $mine = $this->makeDoctor(); $other = $this->makeDoctor(); $clinic = $this->makeClinic(); $this->rule('doctor', $mine->getId(), 'مالِ من'); $this->rule('doctor', $other->getId(), 'مالِ پزشک دیگر'); $this->rule('clinic', $clinic->getId(), 'مالِ کلینیک'); $names = $this->listedNames($mine->getUser()); self::assertContains('مالِ من', $names); self::assertNotContains('مالِ پزشک دیگر', $names); self::assertNotContains('مالِ کلینیک', $names); } public function testClinicManagerSeesOnlyItsClinicRules(): void { $clinic = $this->makeClinic(); $otherClinic = $this->makeClinic(); $doctor = $this->makeDoctor(); $this->rule('clinic', $clinic->getId(), 'کلینیک خودم'); $this->rule('clinic', $otherClinic->getId(), 'کلینیک دیگر'); $this->rule('doctor', $doctor->getId(), 'مطب یک پزشک'); $names = $this->listedNames($clinic->getUser()); self::assertContains('کلینیک خودم', $names); self::assertNotContains('کلینیک دیگر', $names); self::assertNotContains('مطب یک پزشک', $names); } /** پزشکِ عضو در محیط کلینیک، قوانین کلینیک را می‌بیند نه مطب شخصی‌اش. */ public function testMemberDoctorInClinicContextSeesClinicRules(): void { $doctor = $this->makeDoctor(); $clinic = $this->makeClinic(); $this->joinClinic($clinic, $doctor); $this->setClinicContext($doctor->getUser(), $clinic); $this->rule('clinic', $clinic->getId(), 'قانون کلینیک'); $this->rule('doctor', $doctor->getId(), 'قانون مطب شخصی'); $names = $this->listedNames($doctor->getUser()); self::assertContains('قانون کلینیک', $names); self::assertNotContains('قانون مطب شخصی', $names); } /** همان پزشک بیرون از محیط کلینیک، فقط مطب شخصی. */ public function testMemberDoctorOutsideClinicContextSeesOwnPracticeRules(): void { $doctor = $this->makeDoctor(); $clinic = $this->makeClinic(); $this->joinClinic($clinic, $doctor); $this->rule('clinic', $clinic->getId(), 'قانون کلینیک'); $this->rule('doctor', $doctor->getId(), 'قانون مطب شخصی'); $names = $this->listedNames($doctor->getUser()); self::assertContains('قانون مطب شخصی', $names); self::assertNotContains('قانون کلینیک', $names); } /** پزشکی که مالک کلینیک هم هست: محیط فعال تعیین‌کننده است، نه نقش. */ public function testDoctorWhoOwnsClinicSeesClinicRulesInClinicContext(): void { $user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']); $doctor = $this->makeDoctor($user); $clinic = $this->makeClinic($user); $this->joinClinic($clinic, $doctor); $this->setClinicContext($user, $clinic); $this->rule('clinic', $clinic->getId(), 'قانون کلینیکِ خودش'); $this->rule('doctor', $doctor->getId(), 'قانون مطبِ خودش'); $names = $this->listedNames($user); self::assertContains('قانون کلینیکِ خودش', $names); self::assertNotContains('قانون مطبِ خودش', $names); } /** ❌ بدون توکن، لیست اصلاً باز نمی‌شود. */ public function testAnonymousCannotListRules(): void { $this->client->request('GET', self::LIST_URL); self::assertSame(401, $this->client->getResponse()->getStatusCode()); } /** ⚠️ محیط حل‌نشده: کاربر بدون نقش پنل، نه خطای ۵۰۰ می‌گیرد نه دادهٔ کسی را. */ public function testUserWithoutAPanelRoleGetsNoRules(): void { $doctor = $this->makeDoctor(); $this->rule('doctor', $doctor->getId(), 'قانون یک پزشک'); $stranger = $this->createUser(['ROLE_USER']); $this->authJson('GET', self::LIST_URL, $stranger); self::assertContains($this->responseCode(), [403, 404], 'دسترسی رد شود، نه خطای سرور'); } }