Files
clinicpro/tests/Shared/EntityContextResolverTest.php
T
hamedandClaude Opus 5 32044c8fa9 fix(subscription): read the subscription of the environment the user owns
A user who is both a doctor and a clinic owner always resolved to the
doctor: SubscriptionController had its own role-first resolveEntity, and
ownedEntity() returned the doctor whenever one existed. So a subscription
granted to that user's clinic was stored correctly but never surfaced —
/subscription/my kept reporting the free plan and the panel kept the
feature-gated menu items locked.

ownedEntity() now disambiguates with the active context when the user owns
both environments, and the controller delegates to it instead of re-deriving
the pair from roles. Payment already used ownedEntity(), so display, purchase
and admin grant now agree on one environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:07:56 +03:30

311 lines
12 KiB
PHP

<?php
namespace App\Tests\Shared;
use App\Auth\Entity\User;
use App\Auth\Entity\UserActiveContext;
use App\Clinic\Entity\Clinic;
use App\Clinic\Entity\ClinicDoctorPermission;
use App\Doctor\Entity\Doctor;
use App\Secretary\Entity\DoctorSecretary;
use App\Shared\Context\EntityContext;
use App\Shared\Context\EntityContextResolver;
use App\Shared\Exception\AppException;
use App\Tests\ApiTestCase;
/**
* ماتریس نقش‌ها: «این کاربر در کدام محیط کار می‌کند؟»
*
* قرارداد اجرایی فازهای بعدیِ نشانه‌گذاری tenant است — هر assert روی
* toEntityPair() است، نه روی جزئیات داخلی رزولور.
*
* رزولور فقط محیط را تعیین می‌کند؛ مجوزها (ClinicDoctorPermission /
* DoctorSecretary.permission) لایهٔ جداگانه‌اند و اینجا سنجیده نمی‌شوند.
*/
class EntityContextResolverTest extends ApiTestCase
{
private function resolver(): EntityContextResolver
{
return static::getContainer()->get(EntityContextResolver::class);
}
private function makeDoctor(?User $user = null): Doctor
{
$doctor = new Doctor($user ?? $this->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;
}
private function joinClinic(Clinic $clinic, Doctor $doctor): void
{
$clinic->getDoctors()->add($doctor);
$this->em->persist(new ClinicDoctorPermission($clinic, $doctor));
$this->em->flush();
}
private function setActiveContext(User $user, string $dbUuid, string $dbType): void
{
$this->em->persist(new UserActiveContext($user, $dbUuid, $dbType));
$this->em->flush();
}
private function setClinicContext(User $user, Clinic $clinic): void
{
$this->setActiveContext($user, $clinic->getUuid(), EntityContext::TYPE_CLINIC);
}
private function setDoctorContext(User $user, Doctor $doctor): void
{
$this->setActiveContext($user, $doctor->getUuid(), EntityContext::TYPE_DOCTOR);
}
// ── سناریو ۱: پزشک مستقل ────────────────────────────────────────────────
public function testIndependentDoctorAlwaysResolvesToOwnPractice(): void
{
$doctor = $this->makeDoctor();
$context = $this->resolver()->resolve($doctor->getUser());
self::assertSame(['doctor', $doctor->getId()], $context->toEntityPair());
}
// ── سناریو ۲: پزشکِ مستقلِ عضو کلینیک ───────────────────────────────────
public function testClinicMemberDoctorResolvesToClinicWhenActiveContextIsClinic(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
$this->joinClinic($clinic, $doctor);
$this->setClinicContext($doctor->getUser(), $clinic);
$context = $this->resolver()->resolve($doctor->getUser());
self::assertSame(['clinic', $clinic->getId()], $context->toEntityPair());
}
public function testClinicMemberDoctorFallsBackToOwnPracticeWhenActiveContextIsSelf(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
$this->joinClinic($clinic, $doctor);
$this->setDoctorContext($doctor->getUser(), $doctor);
$context = $this->resolver()->resolve($doctor->getUser());
self::assertSame(['doctor', $doctor->getId()], $context->toEntityPair());
}
/**
* مجوزِ غیرفعال محیط را عوض نمی‌کند — عضویت در clinic_doctors تعیین‌کنندهٔ
* «کجا»ست و ClinicDoctorPermission تعیین‌کنندهٔ «چه کاری». محدودسازی در
* PatientRecordScopeResolver اتفاق می‌افتد، نه اینجا.
*/
public function testInactivePermissionStillResolvesToClinicEnvironment(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
$this->joinClinic($clinic, $doctor);
$permission = $this->em->getRepository(ClinicDoctorPermission::class)
->findOneBy(['clinic' => $clinic, 'doctor' => $doctor]);
$permission->setActive(false);
$this->em->flush();
$this->setClinicContext($doctor->getUser(), $clinic);
$context = $this->resolver()->resolve($doctor->getUser());
self::assertSame(['clinic', $clinic->getId()], $context->toEntityPair());
}
// ── سناریو ۳: پزشکی که هم عضو و هم مالک کلینیک است ──────────────────────
public function testDoctorWhoOwnsClinicResolvesToClinicWithoutMembershipRow(): void
{
$user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']);
$doctor = $this->makeDoctor($user);
$clinic = $this->makeClinic($user);
$this->setClinicContext($user, $clinic);
$context = $this->resolver()->resolve($user);
self::assertSame(['clinic', $clinic->getId()], $context->toEntityPair());
self::assertTrue($this->resolver()->canActInClinic($user, $clinic));
}
public function testDoctorWhoOwnsClinicResolvesToOwnPracticeWhenActiveContextIsSelf(): void
{
$user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']);
$doctor = $this->makeDoctor($user);
$this->makeClinic($user);
$this->setDoctorContext($user, $doctor);
$context = $this->resolver()->resolve($user);
self::assertSame(['doctor', $doctor->getId()], $context->toEntityPair());
}
// ── محیطِ صاحب‌شده (اشتراک) ──────────────────────────────────────────────
/**
* اشتراک روی محیطی می‌نشیند که کاربر صاحبش است. کاربری که هم پزشک است و هم
* مالک کلینیک، دو محیط صاحب‌شده دارد و محیط فعال تعیین می‌کند کدام‌یک؛ وگرنه
* اشتراکِ اعطاشده به کلینیک هرگز در پنل دیده نمی‌شود.
*/
public function testOwnedEntityFollowsActiveContextWhenUserOwnsBoth(): void
{
$user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']);
$this->makeDoctor($user);
$clinic = $this->makeClinic($user);
$this->setClinicContext($user, $clinic);
self::assertSame(['clinic', $clinic->getId()], $this->resolver()->ownedEntity($user)->toEntityPair());
}
public function testOwnedEntityIsOwnPracticeWhenActiveContextIsSelf(): void
{
$user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']);
$doctor = $this->makeDoctor($user);
$this->makeClinic($user);
$this->setDoctorContext($user, $doctor);
self::assertSame(['doctor', $doctor->getId()], $this->resolver()->ownedEntity($user)->toEntityPair());
}
/** بدون محیط فعال، مطب شخصی همان پیش‌فرض قبلی می‌ماند. */
public function testOwnedEntityFallsBackToOwnPracticeWithoutActiveContext(): void
{
$user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']);
$doctor = $this->makeDoctor($user);
$this->makeClinic($user);
self::assertSame(['doctor', $doctor->getId()], $this->resolver()->ownedEntity($user)->toEntityPair());
}
/** مالک کلینیکِ غیرپزشک تنها یک محیط دارد و محیط فعال چیزی را عوض نمی‌کند. */
public function testOwnedEntityIsClinicForNonDoctorOwner(): void
{
$clinic = $this->makeClinic();
self::assertSame(['clinic', $clinic->getId()], $this->resolver()->ownedEntity($clinic->getUser())->toEntityPair());
}
// ── سناریو ۴: مدیر/مالک کلینیک (غیرپزشک) ────────────────────────────────
public function testClinicManagerAlwaysResolvesToClinic(): void
{
$clinic = $this->makeClinic();
$context = $this->resolver()->resolve($clinic->getUser());
self::assertSame(['clinic', $clinic->getId()], $context->toEntityPair());
}
// ── سناریو ۵: منشی ──────────────────────────────────────────────────────
public function testSecretaryWithoutActiveContextResolvesToUnknown(): void
{
$user = $this->createUser(['ROLE_SECRETARY']);
$context = $this->resolver()->resolve($user);
self::assertSame(EntityContext::TYPE_UNKNOWN, $context->type);
self::assertFalse($context->isResolved());
}
public function testSecretaryWithActiveClinicRelationResolvesToThatClinic(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
$this->joinClinic($clinic, $doctor);
$secretary = $this->createUser(['ROLE_SECRETARY']);
$this->em->persist(new DoctorSecretary($doctor, $secretary, $clinic));
$this->em->flush();
$this->setClinicContext($secretary, $clinic);
$context = $this->resolver()->resolve($secretary);
self::assertSame(['clinic', $clinic->getId()], $context->toEntityPair());
}
public function testSecretaryWithActiveDoctorRelationResolvesToThatPractice(): void
{
$doctor = $this->makeDoctor();
$secretary = $this->createUser(['ROLE_SECRETARY']);
$this->em->persist(new DoctorSecretary($doctor, $secretary));
$this->em->flush();
$this->setDoctorContext($secretary, $doctor);
$context = $this->resolver()->resolve($secretary);
self::assertSame(['doctor', $doctor->getId()], $context->toEntityPair());
}
/** رابطهٔ غیرفعال = پایان همکاری؛ محیط دیگر حل نمی‌شود. */
public function testSecretaryWithInactiveRelationResolvesToUnknown(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
$this->joinClinic($clinic, $doctor);
$secretary = $this->createUser(['ROLE_SECRETARY']);
$relation = new DoctorSecretary($doctor, $secretary, $clinic);
$relation->setActive(false);
$this->em->persist($relation);
$this->em->flush();
$this->setClinicContext($secretary, $clinic);
$context = $this->resolver()->resolve($secretary);
self::assertFalse($context->isResolved());
}
// ── مسیر clinic_uuid صریح ───────────────────────────────────────────────
public function testExplicitClinicUuidForNonMemberThrowsAccessDenied(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
$this->expectException(AppException::class);
$this->resolver()->resolve($doctor->getUser(), $clinic->getUuid());
}
public function testTryResolveReturnsNullInsteadOfThrowingForNonMember(): void
{
$doctor = $this->makeDoctor();
$clinic = $this->makeClinic();
self::assertNull($this->resolver()->tryResolve($doctor->getUser(), $clinic->getUuid()));
}
public function testExplicitClinicUuidWinsOverStoredActiveContext(): void
{
$doctor = $this->makeDoctor();
$clinicA = $this->makeClinic();
$clinicB = $this->makeClinic();
$this->joinClinic($clinicA, $doctor);
$this->joinClinic($clinicB, $doctor);
$this->setClinicContext($doctor->getUser(), $clinicA);
$context = $this->resolver()->resolve($doctor->getUser(), $clinicB->getUuid());
self::assertSame(['clinic', $clinicB->getId()], $context->toEntityPair());
}
}