Files
clinicpro/tests/Clinic/ClinicContactSourceTest.php
T
hamedandClaude Opus 5 5d2594ff87 feat(privacy): keep venue phone numbers out of every public response
A doctor's office number sat next to the address on the public profile and in
the anonymous API payload, so harvesting the phone number of every practice in
the country was one unauthenticated request away. Street address and map
coordinates stay public — a patient needs those to find the place — but the
phone is now opt-in per caller: DoctorAddress::toArray() and the clinic
serializers only emit it when told to, and the public doctor/clinic endpoints
tell them to only when the caller may edit that profile (the same can_edit they
already compute). Owner-facing address CRUD keeps returning it unchanged.

The patient still gets the number where it is actually useful — their own
appointment. That payload also stops guessing: it used to serialise the
doctor's *first* address, so a booking made at the clinic or at a second office
showed the wrong street entirely. It now resolves the address recorded on the
appointment itself, which works the same for a personal office and a clinic
branch, and falls back to the clinic's own number when the address has none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:31:52 +03:30

122 lines
4.7 KiB
PHP

<?php
namespace App\Tests\Clinic;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\DoctorAddress;
use App\Location\Entity\City;
use App\Location\Entity\Province;
use App\Tests\ApiTestCase;
/**
* GET /api/v1/clinic/{uuid} used to build one response out of two rows: city,
* province and coordinates came from the clinic's DoctorAddress while the street
* text and phone still came from the deprecated columns on the clinic itself.
* A clinic whose address record said Karaj then rendered a Yazd street on the
* public site. Contact fields must all resolve from the address record, and only
* fall back to the legacy columns when that record has nothing to offer.
*
* The phone is a separate matter: it is not public data, so the anonymous
* response must never carry it. The resolution order is still asserted, just
* through the owner's view of the same endpoint.
*/
class ClinicContactSourceTest extends ApiTestCase
{
public function testContactFieldsComeFromTheAddressRecord(): void
{
$clinic = $this->createClinic('یزد، خیابان قدیمی', '03531234567');
$this->createAddressFor($clinic, 'کرج، بلوار طالقانی', '02634567890');
$data = $this->fetchClinic($clinic);
$this->assertSame('کرج، بلوار طالقانی', $data['location']);
$this->assertSame('کرج', $data['city'][0]['name'] ?? null);
$this->assertSame('البرز', $data['state'][0]['name'] ?? null);
$owner = $this->fetchClinicAsOwner($clinic);
$this->assertSame('02634567890', $owner['phone']);
$this->assertSame('02634567890', $owner['phone_number']);
}
public function testThePublicResponseCarriesNoPhoneNumber(): void
{
$clinic = $this->createClinic('یزد، خیابان قدیمی', '03531234567');
$this->createAddressFor($clinic, 'کرج، بلوار طالقانی', '02634567890');
$data = $this->fetchClinic($clinic);
$this->assertNull($data['phone'], 'شمارهٔ کلینیک نباید در پاسخ عمومی بیاید');
$this->assertNull($data['phone_number']);
}
public function testLegacyColumnsAreUsedWhenTheClinicHasNoAddressRecord(): void
{
$clinic = $this->createClinic('یزد، خیابان قدیمی', '03531234567');
$data = $this->fetchClinic($clinic);
$this->assertSame('یزد، خیابان قدیمی', $data['location']);
$this->assertSame([], $data['city']);
$this->assertSame('03531234567', $this->fetchClinicAsOwner($clinic)['phone']);
}
public function testBlankAddressFieldsFallBackInsteadOfBlankingTheContactBlock(): void
{
$clinic = $this->createClinic('یزد، خیابان قدیمی', '03531234567');
$this->createAddressFor($clinic, ' ', null);
$data = $this->fetchClinic($clinic);
$this->assertSame('یزد، خیابان قدیمی', $data['location']);
$this->assertSame('کرج', $data['city'][0]['name'] ?? null);
$this->assertSame('03531234567', $this->fetchClinicAsOwner($clinic)['phone']);
}
private function createClinic(?string $legacyAddress, ?string $legacyPhone): Clinic
{
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
$clinic->setName('کلینیک تست تماس');
$clinic->setAddress($legacyAddress);
$clinic->setTelephone($legacyPhone);
$this->em->persist($clinic);
$this->em->flush();
return $clinic;
}
private function createAddressFor(Clinic $clinic, ?string $street, ?string $phone): DoctorAddress
{
$province = new Province('البرز');
$city = new City('کرج', $province);
$this->em->persist($province);
$this->em->persist($city);
$address = DoctorAddress::forClinic((int) $clinic->getId());
$address->setAddress($street);
$address->setTelephone($phone);
$address->setCity($city);
$address->setProvince($province);
$this->em->persist($address);
$this->em->flush();
return $address;
}
private function fetchClinic(Clinic $clinic): array
{
$this->client->request('GET', '/api/v1/clinic/' . $clinic->getUuid());
$this->assertSame(200, $this->responseCode());
return json_decode($this->client->getResponse()->getContent(), true)['data']['data'];
}
/** همان اندپوینت، این‌بار با توکنِ صاحب کلینیک — جایی که شماره دیده می‌شود. */
private function fetchClinicAsOwner(Clinic $clinic): array
{
$body = $this->authJson('GET', '/api/v1/clinic/' . $clinic->getUuid(), $clinic->getUser());
$this->assertSame(200, $this->responseCode());
return $body['data']['data'];
}
}