get(TenantOwnershipChecker::class); } private function makeDoctor(): Doctor { $doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر مالکیت'); $this->em->persist($doctor); $this->em->flush(); return $doctor; } private function serviceFor(Doctor $doctor): ServiceItem { $section = new ServiceSection('doctor', $doctor->getId(), 'بخش'); $this->em->persist($section); $item = new ServiceItem($section, 'سرویس', 0); $this->em->persist($item); $this->em->flush(); return $item; } public function testEntityOfTheSameEnvironmentBelongs(): void { $doctor = $this->makeDoctor(); self::assertTrue( $this->checker()->belongsTo(EntityContext::forDoctor($doctor), $this->serviceFor($doctor)), ); } public function testEntityOfAnotherEnvironmentDoesNot(): void { $doctor = $this->makeDoctor(); $foreign = $this->serviceFor($this->makeDoctor()); self::assertFalse($this->checker()->belongsTo(EntityContext::forDoctor($doctor), $foreign)); } /** فرزند aggregate جفتش را از ریشه به ارث می‌برد — ServiceItem از ServiceSection. */ public function testAggregateChildIsComparedThroughItsRoot(): void { $doctor = $this->makeDoctor(); $item = $this->serviceFor($doctor); self::assertSame('doctor', $item->getEntityType()); self::assertSame($doctor->getId(), $item->getEntityId()); } /** ❌ null یعنی «پیدا نشد» و همان‌قدر رد می‌شود که یک موجودیت بیگانه. */ public function testNullIsNeverOwned(): void { $doctor = $this->makeDoctor(); self::assertFalse($this->checker()->belongsTo(EntityContext::forDoctor($doctor), null)); self::assertFalse($this->checker()->belongsToPair('doctor', $doctor->getId(), null)); } /** ❌ محیط حل‌نشده هیچ‌چیز را مالک نیست. */ public function testUnresolvedContextOwnsNothing(): void { $item = $this->serviceFor($this->makeDoctor()); self::assertFalse($this->checker()->belongsTo(EntityContext::unknown(), $item)); } /** * ❌ موجودیتی که جفت محیطش را expose نمی‌کند باید بلند خطا بدهد، نه اینکه * بی‌صدا false برگرداند — سکوت اینجا یعنی گاردِ همیشه-بسته که باگ می‌سازد. */ public function testEntityWithoutATenantPairThrows(): void { $doctor = $this->makeDoctor(); $this->expectException(\InvalidArgumentException::class); $this->checker()->belongsTo(EntityContext::forDoctor($doctor), new \stdClass()); } /** ⚠️ یک بیگانه در فهرست، کل فهرست را رد می‌کند. */ public function testOneForeignEntityRejectsTheWholeList(): void { $doctor = $this->makeDoctor(); $mine = $this->serviceFor($doctor); $foreign = $this->serviceFor($this->makeDoctor()); $context = EntityContext::forDoctor($doctor); self::assertTrue($this->checker()->allBelongTo($context, [$mine])); self::assertFalse($this->checker()->allBelongTo($context, [$mine, $foreign])); self::assertTrue($this->checker()->allBelongTo($context, []), 'فهرست خالی مانعی ندارد'); } /** نوعِ محیط هم بخشی از هویت است: کلینیک ۵ با پزشک ۵ یکی نیست. */ public function testTypeIsPartOfTheIdentityNotJustTheId(): void { $doctor = $this->makeDoctor(); $item = $this->serviceFor($doctor); $clinic = new Clinic($this->createUser(['ROLE_CLINIC'])); $clinic->setName('کلینیک'); $this->em->persist($clinic); $this->em->flush(); self::assertFalse( $this->checker()->belongsToPair('clinic', $doctor->getId(), $item), 'همان شناسه ولی نوع دیگر نباید مالک شمرده شود', ); } /** موجودیت‌هایی که خودشان جفت دارند (نه از ریشه) هم با همین checker سنجیده می‌شوند. */ public function testEntitiesCarryingTheirOwnPairWorkToo(): void { $doctor = $this->makeDoctor(); $staff = new ClinicStaff('doctor', $doctor->getId(), 'پرسنل'); $this->em->persist($staff); $this->em->flush(); self::assertTrue($this->checker()->belongsTo(EntityContext::forDoctor($doctor), $staff)); self::assertFalse($this->checker()->belongsToPair('doctor', $doctor->getId() + 1, $staff)); } }