Files
clinicpro/tests/Auth/MultiClinicOwnerContextTest.php
T
hamedandClaude Opus 5 74d2034158 fix(auth): offer every clinic a user owns as a switchable context
buildAvailableContexts used ClinicRepository::findByUser(), which is a
findOneBy — so a user who owns two clinics only ever saw the first one.
switchContext validates its input against that same list, so the second
clinic could not be selected at all.

Before tenant isolation this was merely annoying. Since phase 4 it is a
blocker: an environment that cannot be selected is an environment
TenantFilter hides from its own owner. Found by running the suite against
an imported production database, where one account owns two clinics and
its second clinic had become unreachable.

findByUser() stays for the fallbacks that only need "some clinic"; the
context list now uses findAllByUser(). The other 20 findByUser() call sites
are single-clinic fallbacks used when no context is chosen, and keep their
current behaviour — once the owner can switch, UserActiveContext decides.

Removing the fix turns 3 of the 4 new tests red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:11:49 +03:30

118 lines
4.6 KiB
PHP

<?php
namespace App\Tests\Auth;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\Tests\ApiTestCase;
/**
* مالکِ چند کلینیک باید بتواند به **هر** کدامشان سوییچ کند.
*
* `buildAvailableContexts` قبلاً از `ClinicRepository::findByUser()` استفاده می‌کرد
* که `findOneBy` است و فقط اولین کلینیک را می‌داد. `switchContext` هم ورودی را با
* همان فهرست می‌سنجد، پس کلینیک دوم اصلاً قابل انتخاب نبود.
*
* پیش از جداسازی محیط این فقط آزاردهنده بود؛ با فاز ۴ به بعد تبدیل به مسدودکننده
* شد: محیطی که انتخاب نشود، `TenantFilter` دادهٔ آن را از خودِ مالکش هم پنهان
* می‌کند. روی دادهٔ واقعی production دیده شد، نه در تست‌های ساختگی.
*/
class MultiClinicOwnerContextTest extends ApiTestCase
{
/** @return array{0: User, 1: Clinic, 2: Clinic} */
private function ownerOfTwoClinics(): array
{
$owner = $this->createUser(['ROLE_CLINIC']);
$first = new Clinic($owner);
$first->setName('کلینیک اول');
$this->em->persist($first);
$second = new Clinic($owner);
$second->setName('کلینیک دوم');
$this->em->persist($second);
$this->em->flush();
return [$owner, $first, $second];
}
private function switchTo(User $owner, Clinic $clinic): array
{
return $this->authJson('POST', '/api/v1/auth/switch-context', $owner, [
'db_uuid' => $clinic->getUuid(),
'type' => 'clinic',
]);
}
/**
* ✅ هر دو کلینیک قابل انتخاب‌اند.
*
* از راه خودِ switch سنجیده می‌شود، نه از فهرست: `switchContext` ورودی را با
* همان `buildAvailableContexts` می‌سنجد، پس سوییچِ موفق یعنی محیط در فهرست بوده.
*/
public function testBothOwnedClinicsCanBeSelected(): void
{
[$owner, $first, $second] = $this->ownerOfTwoClinics();
$this->switchTo($owner, $first);
self::assertSame(200, $this->responseCode(), 'کلینیک اول باید قابل انتخاب باشد');
$this->switchTo($owner, $second);
self::assertSame(200, $this->responseCode(), 'کلینیک دوم هم باید قابل انتخاب باشد');
}
/** ✅ سوییچ به کلینیک دوم واقعاً انجام می‌شود. */
public function testTheOwnerCanSwitchToTheSecondClinic(): void
{
[$owner, , $second] = $this->ownerOfTwoClinics();
$res = $this->authJson('POST', '/api/v1/auth/switch-context', $owner, [
'db_uuid' => $second->getUuid(),
'type' => 'clinic',
]);
self::assertSame(200, $this->responseCode());
self::assertSame($second->getUuid(), $res['data']['context']['db_uuid']);
self::assertSame('clinic', $res['data']['context']['role']);
}
/** ❌ کلینیکِ شخص دیگر همچنان قابل انتخاب نیست. */
public function testAnotherOwnersClinicIsStillRefused(): void
{
[$owner] = $this->ownerOfTwoClinics();
[, $strangersClinic] = $this->ownerOfTwoClinics();
$this->authJson('POST', '/api/v1/auth/switch-context', $owner, [
'db_uuid' => $strangersClinic->getUuid(),
'type' => 'clinic',
]);
self::assertSame(403, $this->responseCode());
}
/**
* ⚠️ مرزی: پس از سوییچ، دادهٔ همان کلینیک دیده می‌شود — این همان چیزی است که
* پیش از رفع باگ غیرممکن بود.
*/
public function testDataOfTheSecondClinicBecomesReachableAfterSwitching(): void
{
[$owner, , $second] = $this->ownerOfTwoClinics();
$this->authJson('POST', '/api/v1/auth/switch-context', $owner, [
'db_uuid' => $second->getUuid(),
'type' => 'clinic',
]);
self::assertSame(200, $this->responseCode());
$created = $this->authJson('POST', '/api/v1/my/payment-methods/bank-accounts', $owner, [
'bank_name' => 'ملی',
'account_number' => '0101234567890',
]);
self::assertSame(201, $this->responseCode());
self::assertSame('clinic', $created['data']['entity_type']);
$list = $this->authJson('GET', '/api/v1/my/payment-methods/bank-accounts', $owner);
self::assertSame(['ملی'], array_column($list['data'], 'bank_name'));
}
}